diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index b434351b..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: 2.1 -jobs: - build: - docker: - - image: circleci/golang:latest - steps: - - checkout - - run: mkdir -p /tmp/test-results - - restore_cache: - keys: - - go-mod-v2-{{ checksum "go.sum" }} - - run: if [[ -n $(gofmt -l .) ]]; then echo "Please run gofmt"; exit 1; fi - - run: go vet -v ./... - - run: go get golang.org/x/tools/cmd/goimports - - run: go generate ./... - - run: git update-index --assume-unchanged go.mod - - run: git update-index --assume-unchanged go.sum - - run: if [[ -n $(git status --porcelain) ]]; then echo "Git repo is dirty after runing go generate -- please don't modify generated files"; echo $(git diff);echo $(git status --porcelain); exit 1; fi - - run: gotestsum --junitfile /tmp/test-results/results.xml -- ./... -short -v -mod=mod - - run: go test -run=CSFuzzed -tags=gofuzz ./backend/groth16/... -v - - store_test_results: - path: /tmp/test-results - - save_cache: - key: go-mod-v2-{{ checksum "go.sum" }} - paths: - - "/go/pkg/mod" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c7b54b51..b58f8dcd 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -16,8 +16,8 @@ jobs: - name: install deps run: go install golang.org/x/tools/cmd/goimports@latest && go install github.com/klauspost/asmfmt/cmd/asmfmt@latest - - name: gofmt - run: if [[ -n $(gofmt -l .) ]]; then echo "please run gofmt"; exit 1; fi + - name: goimports + run: if [[ -n $(goimports -l .) ]]; then echo "please run goimports"; exit 1; fi - name: generated files should not be modified run: | go generate ./... diff --git a/README.md b/README.md index 63d746ab..314b46b2 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ You can then toggle on or off icicle acceleration by providing the `WithIcicleAc proof, err := groth16.Prove(ccs, pk, secretWitness) ``` -For more information about prerequisites see the [ICICLE repo](https://github.com/ingonyama-zk/icicle). **NB! ICICLE CUDA kernels are covered by a special license for now. Follow the instructions to download and set up the kernels.** +For more information about prerequisites see the [ICICLE repo](https://github.com/ingonyama-zk/icicle-gnark). ## Citing diff --git a/backend/backend.go b/backend/backend.go index c8e9d4d5..6b3756a5 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -127,7 +127,7 @@ func WithProverKZGFoldingHashFunction(hFunc hash.Hash) ProverOption { // tag and the ICICLE dependencies are properly installed. See [ICICLE] for // installation description. // -// [ICICLE]: https://github.com/ingonyama-zk/icicle +// [ICICLE]: https://github.com/ingonyama-zk/icicle-gnark func WithIcicleAcceleration() ProverOption { return func(pc *ProverConfig) error { pc.Accelerator = "icicle" diff --git a/backend/groth16/bls12-377/mpcsetup/marshal.go b/backend/groth16/bls12-377/mpcsetup/marshal.go index 8add5fa4..a07f27dc 100644 --- a/backend/groth16/bls12-377/mpcsetup/marshal.go +++ b/backend/groth16/bls12-377/mpcsetup/marshal.go @@ -7,11 +7,12 @@ package mpcsetup import ( "encoding/binary" + "io" + curve "github.com/consensys/gnark-crypto/ecc/bls12-377" "github.com/consensys/gnark-crypto/ecc/bls12-377/mpcsetup" "github.com/consensys/gnark/internal/utils" gIo "github.com/consensys/gnark/io" - "io" ) // WriteTo implements io.WriterTo diff --git a/backend/groth16/bls12-377/mpcsetup/phase1.go b/backend/groth16/bls12-377/mpcsetup/phase1.go index 55fe77d5..12fe8aac 100644 --- a/backend/groth16/bls12-377/mpcsetup/phase1.go +++ b/backend/groth16/bls12-377/mpcsetup/phase1.go @@ -10,11 +10,12 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls12-377" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" "github.com/consensys/gnark-crypto/ecc/bls12-377/mpcsetup" - "math/big" ) // SrsCommons are the circuit-independent components of the Groth16 SRS, @@ -213,10 +214,10 @@ func (p *Phase1) Verify(next *Phase1) error { } return mpcsetup.SameRatioMany( - p.parameters.G1.Tau, - p.parameters.G2.Tau, - p.parameters.G1.AlphaTau, - p.parameters.G1.BetaTau, + next.parameters.G1.Tau, + next.parameters.G2.Tau, + next.parameters.G1.AlphaTau, + next.parameters.G1.BetaTau, ) } diff --git a/backend/groth16/bls12-377/mpcsetup/phase2.go b/backend/groth16/bls12-377/mpcsetup/phase2.go index eb5ece5e..04b94e11 100644 --- a/backend/groth16/bls12-377/mpcsetup/phase2.go +++ b/backend/groth16/bls12-377/mpcsetup/phase2.go @@ -10,6 +10,9 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "slices" + curve "github.com/consensys/gnark-crypto/ecc/bls12-377" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" "github.com/consensys/gnark-crypto/ecc/bls12-377/mpcsetup" @@ -18,8 +21,6 @@ import ( "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bls12-377" "github.com/consensys/gnark/internal/utils" - "math/big" - "slices" ) // Phase2Evaluations components of the circuit keys diff --git a/backend/groth16/bls12-377/mpcsetup/setup_test.go b/backend/groth16/bls12-377/mpcsetup/setup_test.go index 217076d7..3afdc433 100644 --- a/backend/groth16/bls12-377/mpcsetup/setup_test.go +++ b/backend/groth16/bls12-377/mpcsetup/setup_test.go @@ -8,17 +8,18 @@ package mpcsetup import ( "bytes" "fmt" - "github.com/consensys/gnark-crypto/ecc" - curve "github.com/consensys/gnark-crypto/ecc/bls12-377" - "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" - groth16Impl "github.com/consensys/gnark/backend/groth16/bls12-377" - cs "github.com/consensys/gnark/constraint/bls12-377" "io" "math/big" "slices" "sync" "testing" + "github.com/consensys/gnark-crypto/ecc" + curve "github.com/consensys/gnark-crypto/ecc/bls12-377" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + groth16Impl "github.com/consensys/gnark/backend/groth16/bls12-377" + cs "github.com/consensys/gnark/constraint/bls12-377" + "github.com/consensys/gnark/backend/groth16" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" diff --git a/backend/groth16/bls12-377/prove.go b/backend/groth16/bls12-377/prove.go index 6ee0df5e..ea5f3f31 100644 --- a/backend/groth16/bls12-377/prove.go +++ b/backend/groth16/bls12-377/prove.go @@ -7,6 +7,10 @@ package groth16 import ( "fmt" + "math/big" + "runtime" + "time" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls12-377" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" @@ -20,9 +24,6 @@ import ( "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/internal/utils" "github.com/consensys/gnark/logger" - "math/big" - "runtime" - "time" fcs "github.com/consensys/gnark/frontend/cs" ) diff --git a/backend/groth16/bls12-377/setup.go b/backend/groth16/bls12-377/setup.go index 539c20eb..77702dd7 100644 --- a/backend/groth16/bls12-377/setup.go +++ b/backend/groth16/bls12-377/setup.go @@ -7,6 +7,9 @@ package groth16 import ( "errors" + "math/big" + "math/bits" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls12-377" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" @@ -15,8 +18,6 @@ import ( "github.com/consensys/gnark/backend/groth16/internal" "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bls12-377" - "math/big" - "math/bits" ) // ProvingKey is used by a Groth16 prover to encode a proof of a statement diff --git a/backend/groth16/bls12-381/mpcsetup/marshal.go b/backend/groth16/bls12-381/mpcsetup/marshal.go index 776b9fac..db8ae0b8 100644 --- a/backend/groth16/bls12-381/mpcsetup/marshal.go +++ b/backend/groth16/bls12-381/mpcsetup/marshal.go @@ -7,11 +7,12 @@ package mpcsetup import ( "encoding/binary" + "io" + curve "github.com/consensys/gnark-crypto/ecc/bls12-381" "github.com/consensys/gnark-crypto/ecc/bls12-381/mpcsetup" "github.com/consensys/gnark/internal/utils" gIo "github.com/consensys/gnark/io" - "io" ) // WriteTo implements io.WriterTo diff --git a/backend/groth16/bls12-381/mpcsetup/phase1.go b/backend/groth16/bls12-381/mpcsetup/phase1.go index e3a616f7..b6233e21 100644 --- a/backend/groth16/bls12-381/mpcsetup/phase1.go +++ b/backend/groth16/bls12-381/mpcsetup/phase1.go @@ -10,11 +10,12 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls12-381" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" "github.com/consensys/gnark-crypto/ecc/bls12-381/mpcsetup" - "math/big" ) // SrsCommons are the circuit-independent components of the Groth16 SRS, @@ -213,10 +214,10 @@ func (p *Phase1) Verify(next *Phase1) error { } return mpcsetup.SameRatioMany( - p.parameters.G1.Tau, - p.parameters.G2.Tau, - p.parameters.G1.AlphaTau, - p.parameters.G1.BetaTau, + next.parameters.G1.Tau, + next.parameters.G2.Tau, + next.parameters.G1.AlphaTau, + next.parameters.G1.BetaTau, ) } diff --git a/backend/groth16/bls12-381/mpcsetup/phase2.go b/backend/groth16/bls12-381/mpcsetup/phase2.go index edfbc68a..85dd2755 100644 --- a/backend/groth16/bls12-381/mpcsetup/phase2.go +++ b/backend/groth16/bls12-381/mpcsetup/phase2.go @@ -10,6 +10,9 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "slices" + curve "github.com/consensys/gnark-crypto/ecc/bls12-381" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" "github.com/consensys/gnark-crypto/ecc/bls12-381/mpcsetup" @@ -18,8 +21,6 @@ import ( "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bls12-381" "github.com/consensys/gnark/internal/utils" - "math/big" - "slices" ) // Phase2Evaluations components of the circuit keys diff --git a/backend/groth16/bls12-381/mpcsetup/setup_test.go b/backend/groth16/bls12-381/mpcsetup/setup_test.go index 4ea56b52..91a3c4d6 100644 --- a/backend/groth16/bls12-381/mpcsetup/setup_test.go +++ b/backend/groth16/bls12-381/mpcsetup/setup_test.go @@ -8,17 +8,18 @@ package mpcsetup import ( "bytes" "fmt" - "github.com/consensys/gnark-crypto/ecc" - curve "github.com/consensys/gnark-crypto/ecc/bls12-381" - "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" - groth16Impl "github.com/consensys/gnark/backend/groth16/bls12-381" - cs "github.com/consensys/gnark/constraint/bls12-381" "io" "math/big" "slices" "sync" "testing" + "github.com/consensys/gnark-crypto/ecc" + curve "github.com/consensys/gnark-crypto/ecc/bls12-381" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + groth16Impl "github.com/consensys/gnark/backend/groth16/bls12-381" + cs "github.com/consensys/gnark/constraint/bls12-381" + "github.com/consensys/gnark/backend/groth16" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" diff --git a/backend/groth16/bls12-381/prove.go b/backend/groth16/bls12-381/prove.go index 497de249..4573814e 100644 --- a/backend/groth16/bls12-381/prove.go +++ b/backend/groth16/bls12-381/prove.go @@ -7,6 +7,10 @@ package groth16 import ( "fmt" + "math/big" + "runtime" + "time" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls12-381" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" @@ -20,9 +24,6 @@ import ( "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/internal/utils" "github.com/consensys/gnark/logger" - "math/big" - "runtime" - "time" fcs "github.com/consensys/gnark/frontend/cs" ) diff --git a/backend/groth16/bls12-381/setup.go b/backend/groth16/bls12-381/setup.go index 53b968ed..12326f7d 100644 --- a/backend/groth16/bls12-381/setup.go +++ b/backend/groth16/bls12-381/setup.go @@ -7,6 +7,9 @@ package groth16 import ( "errors" + "math/big" + "math/bits" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls12-381" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" @@ -15,8 +18,6 @@ import ( "github.com/consensys/gnark/backend/groth16/internal" "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bls12-381" - "math/big" - "math/bits" ) // ProvingKey is used by a Groth16 prover to encode a proof of a statement diff --git a/backend/groth16/bls24-315/mpcsetup/marshal.go b/backend/groth16/bls24-315/mpcsetup/marshal.go index 09fc21b8..3efb0ad1 100644 --- a/backend/groth16/bls24-315/mpcsetup/marshal.go +++ b/backend/groth16/bls24-315/mpcsetup/marshal.go @@ -7,11 +7,12 @@ package mpcsetup import ( "encoding/binary" + "io" + curve "github.com/consensys/gnark-crypto/ecc/bls24-315" "github.com/consensys/gnark-crypto/ecc/bls24-315/mpcsetup" "github.com/consensys/gnark/internal/utils" gIo "github.com/consensys/gnark/io" - "io" ) // WriteTo implements io.WriterTo diff --git a/backend/groth16/bls24-315/mpcsetup/phase1.go b/backend/groth16/bls24-315/mpcsetup/phase1.go index 6dc9bee0..b2d010ee 100644 --- a/backend/groth16/bls24-315/mpcsetup/phase1.go +++ b/backend/groth16/bls24-315/mpcsetup/phase1.go @@ -10,11 +10,12 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls24-315" "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" "github.com/consensys/gnark-crypto/ecc/bls24-315/mpcsetup" - "math/big" ) // SrsCommons are the circuit-independent components of the Groth16 SRS, @@ -213,10 +214,10 @@ func (p *Phase1) Verify(next *Phase1) error { } return mpcsetup.SameRatioMany( - p.parameters.G1.Tau, - p.parameters.G2.Tau, - p.parameters.G1.AlphaTau, - p.parameters.G1.BetaTau, + next.parameters.G1.Tau, + next.parameters.G2.Tau, + next.parameters.G1.AlphaTau, + next.parameters.G1.BetaTau, ) } diff --git a/backend/groth16/bls24-315/mpcsetup/phase2.go b/backend/groth16/bls24-315/mpcsetup/phase2.go index 9efd4e7e..eef46d46 100644 --- a/backend/groth16/bls24-315/mpcsetup/phase2.go +++ b/backend/groth16/bls24-315/mpcsetup/phase2.go @@ -10,6 +10,9 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "slices" + curve "github.com/consensys/gnark-crypto/ecc/bls24-315" "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" "github.com/consensys/gnark-crypto/ecc/bls24-315/mpcsetup" @@ -18,8 +21,6 @@ import ( "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bls24-315" "github.com/consensys/gnark/internal/utils" - "math/big" - "slices" ) // Phase2Evaluations components of the circuit keys diff --git a/backend/groth16/bls24-315/mpcsetup/setup_test.go b/backend/groth16/bls24-315/mpcsetup/setup_test.go index 89b361f3..e2c248c5 100644 --- a/backend/groth16/bls24-315/mpcsetup/setup_test.go +++ b/backend/groth16/bls24-315/mpcsetup/setup_test.go @@ -8,17 +8,18 @@ package mpcsetup import ( "bytes" "fmt" - "github.com/consensys/gnark-crypto/ecc" - curve "github.com/consensys/gnark-crypto/ecc/bls24-315" - "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" - groth16Impl "github.com/consensys/gnark/backend/groth16/bls24-315" - cs "github.com/consensys/gnark/constraint/bls24-315" "io" "math/big" "slices" "sync" "testing" + "github.com/consensys/gnark-crypto/ecc" + curve "github.com/consensys/gnark-crypto/ecc/bls24-315" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" + groth16Impl "github.com/consensys/gnark/backend/groth16/bls24-315" + cs "github.com/consensys/gnark/constraint/bls24-315" + "github.com/consensys/gnark/backend/groth16" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" diff --git a/backend/groth16/bls24-315/prove.go b/backend/groth16/bls24-315/prove.go index f610b946..684c7ca4 100644 --- a/backend/groth16/bls24-315/prove.go +++ b/backend/groth16/bls24-315/prove.go @@ -7,6 +7,10 @@ package groth16 import ( "fmt" + "math/big" + "runtime" + "time" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls24-315" "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" @@ -20,9 +24,6 @@ import ( "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/internal/utils" "github.com/consensys/gnark/logger" - "math/big" - "runtime" - "time" fcs "github.com/consensys/gnark/frontend/cs" ) diff --git a/backend/groth16/bls24-315/setup.go b/backend/groth16/bls24-315/setup.go index 60265255..d0c12f71 100644 --- a/backend/groth16/bls24-315/setup.go +++ b/backend/groth16/bls24-315/setup.go @@ -7,6 +7,9 @@ package groth16 import ( "errors" + "math/big" + "math/bits" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls24-315" "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" @@ -15,8 +18,6 @@ import ( "github.com/consensys/gnark/backend/groth16/internal" "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bls24-315" - "math/big" - "math/bits" ) // ProvingKey is used by a Groth16 prover to encode a proof of a statement diff --git a/backend/groth16/bls24-317/mpcsetup/marshal.go b/backend/groth16/bls24-317/mpcsetup/marshal.go index de91ea80..b76b7840 100644 --- a/backend/groth16/bls24-317/mpcsetup/marshal.go +++ b/backend/groth16/bls24-317/mpcsetup/marshal.go @@ -7,11 +7,12 @@ package mpcsetup import ( "encoding/binary" + "io" + curve "github.com/consensys/gnark-crypto/ecc/bls24-317" "github.com/consensys/gnark-crypto/ecc/bls24-317/mpcsetup" "github.com/consensys/gnark/internal/utils" gIo "github.com/consensys/gnark/io" - "io" ) // WriteTo implements io.WriterTo diff --git a/backend/groth16/bls24-317/mpcsetup/phase1.go b/backend/groth16/bls24-317/mpcsetup/phase1.go index b14505f1..c46133f0 100644 --- a/backend/groth16/bls24-317/mpcsetup/phase1.go +++ b/backend/groth16/bls24-317/mpcsetup/phase1.go @@ -10,11 +10,12 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls24-317" "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" "github.com/consensys/gnark-crypto/ecc/bls24-317/mpcsetup" - "math/big" ) // SrsCommons are the circuit-independent components of the Groth16 SRS, @@ -213,10 +214,10 @@ func (p *Phase1) Verify(next *Phase1) error { } return mpcsetup.SameRatioMany( - p.parameters.G1.Tau, - p.parameters.G2.Tau, - p.parameters.G1.AlphaTau, - p.parameters.G1.BetaTau, + next.parameters.G1.Tau, + next.parameters.G2.Tau, + next.parameters.G1.AlphaTau, + next.parameters.G1.BetaTau, ) } diff --git a/backend/groth16/bls24-317/mpcsetup/phase2.go b/backend/groth16/bls24-317/mpcsetup/phase2.go index 2e356110..f06891f8 100644 --- a/backend/groth16/bls24-317/mpcsetup/phase2.go +++ b/backend/groth16/bls24-317/mpcsetup/phase2.go @@ -10,6 +10,9 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "slices" + curve "github.com/consensys/gnark-crypto/ecc/bls24-317" "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" "github.com/consensys/gnark-crypto/ecc/bls24-317/mpcsetup" @@ -18,8 +21,6 @@ import ( "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bls24-317" "github.com/consensys/gnark/internal/utils" - "math/big" - "slices" ) // Phase2Evaluations components of the circuit keys diff --git a/backend/groth16/bls24-317/mpcsetup/setup_test.go b/backend/groth16/bls24-317/mpcsetup/setup_test.go index 016d1da8..ce48c9a6 100644 --- a/backend/groth16/bls24-317/mpcsetup/setup_test.go +++ b/backend/groth16/bls24-317/mpcsetup/setup_test.go @@ -8,17 +8,18 @@ package mpcsetup import ( "bytes" "fmt" - "github.com/consensys/gnark-crypto/ecc" - curve "github.com/consensys/gnark-crypto/ecc/bls24-317" - "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" - groth16Impl "github.com/consensys/gnark/backend/groth16/bls24-317" - cs "github.com/consensys/gnark/constraint/bls24-317" "io" "math/big" "slices" "sync" "testing" + "github.com/consensys/gnark-crypto/ecc" + curve "github.com/consensys/gnark-crypto/ecc/bls24-317" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" + groth16Impl "github.com/consensys/gnark/backend/groth16/bls24-317" + cs "github.com/consensys/gnark/constraint/bls24-317" + "github.com/consensys/gnark/backend/groth16" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" diff --git a/backend/groth16/bls24-317/prove.go b/backend/groth16/bls24-317/prove.go index 41f858d9..311ccb95 100644 --- a/backend/groth16/bls24-317/prove.go +++ b/backend/groth16/bls24-317/prove.go @@ -7,6 +7,10 @@ package groth16 import ( "fmt" + "math/big" + "runtime" + "time" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls24-317" "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" @@ -20,9 +24,6 @@ import ( "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/internal/utils" "github.com/consensys/gnark/logger" - "math/big" - "runtime" - "time" fcs "github.com/consensys/gnark/frontend/cs" ) diff --git a/backend/groth16/bls24-317/setup.go b/backend/groth16/bls24-317/setup.go index 68812bfa..78f4a914 100644 --- a/backend/groth16/bls24-317/setup.go +++ b/backend/groth16/bls24-317/setup.go @@ -7,6 +7,9 @@ package groth16 import ( "errors" + "math/big" + "math/bits" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls24-317" "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" @@ -15,8 +18,6 @@ import ( "github.com/consensys/gnark/backend/groth16/internal" "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bls24-317" - "math/big" - "math/bits" ) // ProvingKey is used by a Groth16 prover to encode a proof of a statement diff --git a/backend/groth16/bn254/icicle/device.go b/backend/groth16/bn254/icicle/device.go index d00fd1cd..88a5d9f7 100644 --- a/backend/groth16/bn254/icicle/device.go +++ b/backend/groth16/bn254/icicle/device.go @@ -7,7 +7,7 @@ import ( "sync" "github.com/consensys/gnark/logger" - icicle_runtime "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" + icicle_runtime "github.com/ingonyama-zk/icicle-gnark/v3/wrappers/golang/runtime" ) var onceWarmUpDevice sync.Once @@ -22,7 +22,11 @@ func warmUpDevice() { device := icicle_runtime.CreateDevice("CUDA", 0) log.Debug().Int32("id", device.Id).Str("type", device.GetDeviceType()).Msg("ICICLE device created") icicle_runtime.RunOnDevice(&device, func(args ...any) { - err := icicle_runtime.WarmUpDevice() + stream, err := icicle_runtime.CreateStream() + if err != icicle_runtime.Success { + panic(fmt.Sprintf("ICICLE create stream error: %s", err.AsString())) + } + err = icicle_runtime.WarmUpDevice(stream) if err != icicle_runtime.Success { panic(fmt.Sprintf("ICICLE device warmup error: %s", err.AsString())) } diff --git a/backend/groth16/bn254/icicle/icicle.go b/backend/groth16/bn254/icicle/icicle.go index 49133cd8..31beb5a6 100644 --- a/backend/groth16/bn254/icicle/icicle.go +++ b/backend/groth16/bn254/icicle/icicle.go @@ -25,13 +25,13 @@ import ( "github.com/consensys/gnark/internal/utils" "github.com/consensys/gnark/logger" - icicle_core "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" - icicle_bn254 "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254" - icicle_g2 "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/g2" - icicle_msm "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/msm" - icicle_ntt "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/ntt" - icicle_vecops "github.com/ingonyama-zk/icicle/v3/wrappers/golang/curves/bn254/vecOps" - icicle_runtime "github.com/ingonyama-zk/icicle/v3/wrappers/golang/runtime" + icicle_core "github.com/ingonyama-zk/icicle-gnark/v3/wrappers/golang/core" + icicle_bn254 "github.com/ingonyama-zk/icicle-gnark/v3/wrappers/golang/curves/bn254" + icicle_g2 "github.com/ingonyama-zk/icicle-gnark/v3/wrappers/golang/curves/bn254/g2" + icicle_msm "github.com/ingonyama-zk/icicle-gnark/v3/wrappers/golang/curves/bn254/msm" + icicle_ntt "github.com/ingonyama-zk/icicle-gnark/v3/wrappers/golang/curves/bn254/ntt" + icicle_vecops "github.com/ingonyama-zk/icicle-gnark/v3/wrappers/golang/curves/bn254/vecOps" + icicle_runtime "github.com/ingonyama-zk/icicle-gnark/v3/wrappers/golang/runtime" fcs "github.com/consensys/gnark/frontend/cs" ) diff --git a/backend/groth16/bn254/icicle/provingkey.go b/backend/groth16/bn254/icicle/provingkey.go index 0f25a043..625f402a 100644 --- a/backend/groth16/bn254/icicle/provingkey.go +++ b/backend/groth16/bn254/icicle/provingkey.go @@ -6,7 +6,7 @@ import ( "github.com/consensys/gnark-crypto/ecc/bn254/fr" groth16_bn254 "github.com/consensys/gnark/backend/groth16/bn254" cs "github.com/consensys/gnark/constraint/bn254" - icicle_core "github.com/ingonyama-zk/icicle/v3/wrappers/golang/core" + icicle_core "github.com/ingonyama-zk/icicle-gnark/v3/wrappers/golang/core" ) type deviceInfo struct { diff --git a/backend/groth16/bn254/mpcsetup/marshal.go b/backend/groth16/bn254/mpcsetup/marshal.go index 2ca77864..23642e06 100644 --- a/backend/groth16/bn254/mpcsetup/marshal.go +++ b/backend/groth16/bn254/mpcsetup/marshal.go @@ -7,11 +7,12 @@ package mpcsetup import ( "encoding/binary" + "io" + curve "github.com/consensys/gnark-crypto/ecc/bn254" "github.com/consensys/gnark-crypto/ecc/bn254/mpcsetup" "github.com/consensys/gnark/internal/utils" gIo "github.com/consensys/gnark/io" - "io" ) // WriteTo implements io.WriterTo diff --git a/backend/groth16/bn254/mpcsetup/phase1.go b/backend/groth16/bn254/mpcsetup/phase1.go index 466fd5e8..3f08b2cd 100644 --- a/backend/groth16/bn254/mpcsetup/phase1.go +++ b/backend/groth16/bn254/mpcsetup/phase1.go @@ -10,11 +10,12 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bn254" "github.com/consensys/gnark-crypto/ecc/bn254/fr" "github.com/consensys/gnark-crypto/ecc/bn254/mpcsetup" - "math/big" ) // SrsCommons are the circuit-independent components of the Groth16 SRS, @@ -213,10 +214,10 @@ func (p *Phase1) Verify(next *Phase1) error { } return mpcsetup.SameRatioMany( - p.parameters.G1.Tau, - p.parameters.G2.Tau, - p.parameters.G1.AlphaTau, - p.parameters.G1.BetaTau, + next.parameters.G1.Tau, + next.parameters.G2.Tau, + next.parameters.G1.AlphaTau, + next.parameters.G1.BetaTau, ) } diff --git a/backend/groth16/bn254/mpcsetup/phase2.go b/backend/groth16/bn254/mpcsetup/phase2.go index 44304b34..ace3d8db 100644 --- a/backend/groth16/bn254/mpcsetup/phase2.go +++ b/backend/groth16/bn254/mpcsetup/phase2.go @@ -10,6 +10,9 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "slices" + curve "github.com/consensys/gnark-crypto/ecc/bn254" "github.com/consensys/gnark-crypto/ecc/bn254/fr" "github.com/consensys/gnark-crypto/ecc/bn254/mpcsetup" @@ -18,8 +21,6 @@ import ( "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bn254" "github.com/consensys/gnark/internal/utils" - "math/big" - "slices" ) // Phase2Evaluations components of the circuit keys diff --git a/backend/groth16/bn254/mpcsetup/setup_test.go b/backend/groth16/bn254/mpcsetup/setup_test.go index 764b3f72..f765b557 100644 --- a/backend/groth16/bn254/mpcsetup/setup_test.go +++ b/backend/groth16/bn254/mpcsetup/setup_test.go @@ -8,17 +8,18 @@ package mpcsetup import ( "bytes" "fmt" - "github.com/consensys/gnark-crypto/ecc" - curve "github.com/consensys/gnark-crypto/ecc/bn254" - "github.com/consensys/gnark-crypto/ecc/bn254/fr" - groth16Impl "github.com/consensys/gnark/backend/groth16/bn254" - cs "github.com/consensys/gnark/constraint/bn254" "io" "math/big" "slices" "sync" "testing" + "github.com/consensys/gnark-crypto/ecc" + curve "github.com/consensys/gnark-crypto/ecc/bn254" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + groth16Impl "github.com/consensys/gnark/backend/groth16/bn254" + cs "github.com/consensys/gnark/constraint/bn254" + "github.com/consensys/gnark/backend/groth16" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" diff --git a/backend/groth16/bn254/prove.go b/backend/groth16/bn254/prove.go index f7eea593..8ab620bc 100644 --- a/backend/groth16/bn254/prove.go +++ b/backend/groth16/bn254/prove.go @@ -6,7 +6,15 @@ package groth16 import ( + "encoding/binary" "fmt" + "math/big" + "os" + "runtime" + "time" + + "bytes" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bn254" "github.com/consensys/gnark-crypto/ecc/bn254/fr" @@ -20,9 +28,6 @@ import ( "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/internal/utils" "github.com/consensys/gnark/logger" - "math/big" - "runtime" - "time" fcs "github.com/consensys/gnark/frontend/cs" ) @@ -47,8 +52,75 @@ func (proof *Proof) CurveID() ecc.ID { return curve.ID } +func write_G1_to_wasm_array(array []curve.G1Affine) []byte { + array_length := len(array) + wasm_array := make([]byte, array_length*64+8) + lengthBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(lengthBytes, uint64(array_length)) + copy(wasm_array[:8], lengthBytes) + + for i, element := range array { + elementX := element.X + elementY := element.Y + elementXBytes := elementX.BytesMont() + elementYBytes := elementY.BytesMont() + // Keep as little-endian (no byte reversal needed) + copy(wasm_array[8+i*64:8+i*64+32], elementXBytes[:]) + copy(wasm_array[8+i*64+32:8+i*64+64], elementYBytes[:]) + } + return wasm_array +} + +func write_G2_to_wasm_array(array []curve.G2Affine) []byte { + array_length := len(array) + wasm_array := make([]byte, array_length*128+8) + lengthBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(lengthBytes, uint64(array_length)) + copy(wasm_array[:8], lengthBytes) + + for i, element := range array { + elementX := element.X + elementY := element.Y + elementXA0 := elementX.A0 + elementXA1 := elementX.A1 + elementYA0 := elementY.A0 + elementYA1 := elementY.A1 + elementXA0Bytes := elementXA0.BytesMont() + elementXA1Bytes := elementXA1.BytesMont() + elementYA0Bytes := elementYA0.BytesMont() + elementYA1Bytes := elementYA1.BytesMont() + // Keep as little-endian (no byte reversal needed) + copy(wasm_array[8+i*128:8+i*128+32], elementXA0Bytes[:]) + copy(wasm_array[8+i*128+32:8+i*128+64], elementXA1Bytes[:]) + copy(wasm_array[8+i*128+64:8+i*128+96], elementYA0Bytes[:]) + copy(wasm_array[8+i*128+96:8+i*128+128], elementYA1Bytes[:]) + } + return wasm_array +} + +func write_to_wasm_array(array []fr.Element) []byte { + array_length := len(array) + wasm_array := make([]byte, array_length*32+8) + // Write array length as 32-bit little endian + lengthBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(lengthBytes, uint64(array_length)) + copy(wasm_array[:8], lengthBytes) + + // Write each fr.Element as little endian bytes + for i, element := range array { + // Get element bytes in Montgomery form (already little-endian) + elementBytes := element.BytesMont() + // Keep as little-endian (no byte reversal needed) + copy(wasm_array[8+i*32:8+(i+1)*32], elementBytes[:]) + } + return wasm_array +} + // Prove generates the proof of knowledge of a r1cs with full witness (secret + public part). func Prove(r1cs *cs.R1CS, pk *ProvingKey, fullWitness witness.Witness, opts ...backend.ProverOption) (*Proof, error) { + if os.Getenv("DISABLE_GOROUTINE") == "1" { + return serialProve(r1cs, pk, fullWitness, opts...) + } opt, err := backend.NewProverConfig(opts...) if err != nil { return nil, fmt.Errorf("new prover config: %w", err) @@ -313,6 +385,551 @@ func Prove(r1cs *cs.R1CS, pk *ProvingKey, fullWitness witness.Witness, opts ...b return proof, nil } +// ExtractIntermediateData extracts intermediate data from the proving process. +func ExtractIntermediateData(r1cs *cs.R1CS, pk *ProvingKey, fullWitness witness.Witness, opts ...backend.ProverOption) (*GnarkOutput, error) { + gnarkOutput := new(GnarkOutput) + + // Capture ProvingKey G1 generators + alphaBytes := pk.G1.Alpha.X.BytesMont() + alphaBytesY := pk.G1.Alpha.Y.BytesMont() + gnarkOutput.PkG1Alpha = append(alphaBytes[:], alphaBytesY[:]...) + + betaBytes := pk.G1.Beta.X.BytesMont() + betaBytesY := pk.G1.Beta.Y.BytesMont() + gnarkOutput.PkG1Beta = append(betaBytes[:], betaBytesY[:]...) + + deltaBytes := pk.G1.Delta.X.BytesMont() + deltaBytesY := pk.G1.Delta.Y.BytesMont() + gnarkOutput.PkG1Delta = append(deltaBytes[:], deltaBytesY[:]...) + + // Capture ProvingKey G2 generators + g2BetaXA0 := pk.G2.Beta.X.A0.BytesMont() + g2BetaXA1 := pk.G2.Beta.X.A1.BytesMont() + g2BetaYA0 := pk.G2.Beta.Y.A0.BytesMont() + g2BetaYA1 := pk.G2.Beta.Y.A1.BytesMont() + gnarkOutput.PkG2Beta = append(g2BetaXA0[:], g2BetaXA1[:]...) + gnarkOutput.PkG2Beta = append(gnarkOutput.PkG2Beta, g2BetaYA0[:]...) + gnarkOutput.PkG2Beta = append(gnarkOutput.PkG2Beta, g2BetaYA1[:]...) + + g2DeltaXA0 := pk.G2.Delta.X.A0.BytesMont() + g2DeltaXA1 := pk.G2.Delta.X.A1.BytesMont() + g2DeltaYA0 := pk.G2.Delta.Y.A0.BytesMont() + g2DeltaYA1 := pk.G2.Delta.Y.A1.BytesMont() + gnarkOutput.PkG2Delta = append(g2DeltaXA0[:], g2DeltaXA1[:]...) + gnarkOutput.PkG2Delta = append(gnarkOutput.PkG2Delta, g2DeltaYA0[:]...) + gnarkOutput.PkG2Delta = append(gnarkOutput.PkG2Delta, g2DeltaYA1[:]...) + + // Capture Domain information + cardinalityBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(cardinalityBytes, uint64(pk.Domain.Cardinality)) + gnarkOutput.DomainCardinality = cardinalityBytes + generatorBytes := pk.Domain.Generator.BytesMont() + gnarkOutput.DomainGenerator = generatorBytes[:] + + // Capture Infinity masks + infinityABytes := make([]byte, len(pk.InfinityA)+8) + infinityALength := len(pk.InfinityA) + lengthBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(lengthBytes, uint64(infinityALength)) + copy(infinityABytes[:8], lengthBytes) + for i, b := range pk.InfinityA { + if b { + infinityABytes[8+i] = 1 + } else { + infinityABytes[8+i] = 0 + } + } + gnarkOutput.InfinityA = infinityABytes + + infinityBBytes := make([]byte, len(pk.InfinityB)+8) + infinityBBytesLength := len(pk.InfinityB) + lengthBytes = make([]byte, 8) + binary.LittleEndian.PutUint64(lengthBytes, uint64(infinityBBytesLength)) + copy(infinityBBytes[:8], lengthBytes) + for i, b := range pk.InfinityB { + if b { + infinityBBytes[8+i] = 1 + } else { + infinityBBytes[8+i] = 0 + } + } + gnarkOutput.InfinityB = infinityBBytes + + nbInfinityABytes := make([]byte, 8) + binary.LittleEndian.PutUint64(nbInfinityABytes, pk.NbInfinityA) + gnarkOutput.NbInfinityA = nbInfinityABytes + + nbInfinityBBytes := make([]byte, 8) + binary.LittleEndian.PutUint64(nbInfinityBBytes, pk.NbInfinityB) + gnarkOutput.NbInfinityB = nbInfinityBBytes + + opt, err := backend.NewProverConfig(opts...) + if err != nil { + return nil, fmt.Errorf("new prover config: %w", err) + } + if opt.HashToFieldFn == nil { + opt.HashToFieldFn = hash_to_field.New([]byte(constraint.CommitmentDst)) + } + + log := logger.Logger().With().Str("curve", r1cs.CurveID().String()).Str("acceleration", "none").Int("nbConstraints", r1cs.GetNbConstraints()).Str("backend", "groth16").Logger() + + commitmentInfo := r1cs.CommitmentInfo.(constraint.Groth16Commitments) + + proof := &Proof{Commitments: make([]curve.G1Affine, len(commitmentInfo))} + + solverOpts := opt.SolverOpts[:len(opt.SolverOpts):len(opt.SolverOpts)] + + privateCommittedValues := make([][]fr.Element, len(commitmentInfo)) + + // override hints + bsb22ID := solver.GetHintID(fcs.Bsb22CommitmentComputePlaceholder) + solverOpts = append(solverOpts, solver.OverrideHint(bsb22ID, func(_ *big.Int, in []*big.Int, out []*big.Int) error { + i := int(in[0].Int64()) + in = in[1:] + privateCommittedValues[i] = make([]fr.Element, len(commitmentInfo[i].PrivateCommitted)) + hashed := in[:len(commitmentInfo[i].PublicAndCommitmentCommitted)] + committed := in[+len(hashed):] + for j, inJ := range committed { + privateCommittedValues[i][j].SetBigInt(inJ) + } + + var err error + if proof.Commitments[i], err = pk.CommitmentKeys[i].Commit(privateCommittedValues[i]); err != nil { + return err + } + + opt.HashToFieldFn.Write(constraint.SerializeCommitment(proof.Commitments[i].Marshal(), hashed, (fr.Bits-1)/8+1)) + hashBts := opt.HashToFieldFn.Sum(nil) + opt.HashToFieldFn.Reset() + nbBuf := fr.Bytes + if opt.HashToFieldFn.Size() < fr.Bytes { + nbBuf = opt.HashToFieldFn.Size() + } + var res fr.Element + res.SetBytes(hashBts[:nbBuf]) + res.BigInt(out[0]) + return nil + })) + + _solution, err := r1cs.Solve(fullWitness, solverOpts...) + if err != nil { + return nil, err + } + + solution := _solution.(*cs.R1CSSolution) + wireValues := []fr.Element(solution.W) + + start := time.Now() + poks := make([]curve.G1Affine, len(pk.CommitmentKeys)) + + for i := range pk.CommitmentKeys { + var err error + if poks[i], err = pk.CommitmentKeys[i].ProveKnowledge(privateCommittedValues[i]); err != nil { + return nil, err + } + } + // compute challenge for folding the PoKs from the commitments + commitmentsSerialized := make([]byte, fr.Bytes*len(commitmentInfo)) + for i := range commitmentInfo { + copy(commitmentsSerialized[fr.Bytes*i:], wireValues[commitmentInfo[i].CommitmentIndex].Marshal()) + } + challenge, err := fr.Hash(commitmentsSerialized, []byte("G16-BSB22"), 1) + if err != nil { + return nil, err + } + if _, err = proof.CommitmentPok.Fold(poks, challenge[0], ecc.MultiExpConfig{NbTasks: 1}); err != nil { + return nil, err + } + + chHDone := make(chan struct{}, 1) + go func() { + gnarkOutput.SolutionA = write_to_wasm_array(solution.A) + gnarkOutput.SolutionB = write_to_wasm_array(solution.B) + gnarkOutput.SolutionC = write_to_wasm_array(solution.C) + + DomainFrMultiplicativeGenBytes := pk.Domain.FrMultiplicativeGen.BytesMont() + gnarkOutput.DomainFrMultiplicativeGen = DomainFrMultiplicativeGenBytes[:] + DomainFrMultiplicativeGenInvBytes := pk.Domain.FrMultiplicativeGenInv.BytesMont() + gnarkOutput.DomainFrMultiplicativeGenInv = DomainFrMultiplicativeGenInvBytes[:] + DomainCardinalityInvBytes := pk.Domain.CardinalityInv.BytesMont() + gnarkOutput.DomainCardinalityInv = DomainCardinalityInvBytes[:] + DomainGeneratorInvBytes := pk.Domain.GeneratorInv.BytesMont() + gnarkOutput.DomainGeneratorInv = DomainGeneratorInvBytes[:] + + solution.A = nil + solution.B = nil + solution.C = nil + chHDone <- struct{}{} + }() + + // we need to copy and filter the wireValues for each multi exp + // as pk.G1.A, pk.G1.B and pk.G2.B may have (a significant) number of point at infinity + var wireValuesA, wireValuesB []fr.Element + chWireValuesA, chWireValuesB := make(chan struct{}, 1), make(chan struct{}, 1) + + go func() { + wireValuesA = make([]fr.Element, len(wireValues)-int(pk.NbInfinityA)) + for i, j := 0, 0; j < len(wireValuesA); i++ { + if pk.InfinityA[i] { + continue + } + wireValuesA[j] = wireValues[i] + j++ + } + close(chWireValuesA) + gnarkOutput.WireValuesA = write_to_wasm_array(wireValuesA) + + }() + go func() { + wireValuesB = make([]fr.Element, len(wireValues)-int(pk.NbInfinityB)) + for i, j := 0, 0; j < len(wireValuesB); i++ { + if pk.InfinityB[i] { + continue + } + wireValuesB[j] = wireValues[i] + j++ + } + close(chWireValuesB) + gnarkOutput.WireValuesB = write_to_wasm_array(wireValuesB) + + }() + + // sample random r and s + var r, s big.Int + var _r, _s, _kr fr.Element + if _, err := _r.SetRandom(); err != nil { + return nil, err + } + if _, err := _s.SetRandom(); err != nil { + return nil, err + } + _kr.Mul(&_r, &_s).Neg(&_kr) + + _r.BigInt(&r) + _s.BigInt(&s) + + rBytes := _r.BytesMont() + gnarkOutput.R = rBytes[:] + sBytes := _s.BytesMont() + gnarkOutput.S = sBytes[:] + krBytes := _kr.BytesMont() + gnarkOutput.Kr = krBytes[:] + + gnarkOutput.PkB = write_G1_to_wasm_array(pk.G1.B) + + gnarkOutput.PkA = write_G1_to_wasm_array(pk.G1.A) + + gnarkOutput.PkZ = write_G1_to_wasm_array(pk.G1.Z) + gnarkOutput.PkK = write_G1_to_wasm_array(pk.G1.K) + + // filter the wire values if needed + // TODO Perf @Tabaie worst memory allocation offender + toRemove := commitmentInfo.GetPrivateCommitted() + toRemove = append(toRemove, commitmentInfo.CommitmentIndexes()) + _wireValues := filterHeap(wireValues[r1cs.GetNbPublicVariables():], r1cs.GetNbPublicVariables(), internal.ConcatAll(toRemove...)) + gnarkOutput.WireValuesFiltered = write_to_wasm_array(_wireValues) + + gnarkOutput.PkG2B = write_G2_to_wasm_array(pk.G2.B) + + // wait for FFT to end, as it uses all our CPUs + <-chHDone + + log.Debug().Dur("took", time.Since(start)).Msg("prover done") + + return gnarkOutput, nil +} + +type GnarkOutput struct { + /* Intermediate data */ + SolutionA []byte + SolutionB []byte + SolutionC []byte + WireValuesA []byte + WireValuesB []byte + WireValuesFiltered []byte + R []byte + S []byte + Kr []byte + + /* ProvingKey elements */ + PkA []byte + PkB []byte + PkZ []byte + PkK []byte + PkG2B []byte + PkG1Alpha []byte + PkG1Beta []byte + PkG1Delta []byte + PkG2Beta []byte + PkG2Delta []byte + + // Domain information + DomainCardinality []byte + DomainCardinalityInv []byte + DomainGenerator []byte + DomainGeneratorInv []byte + DomainFrMultiplicativeGen []byte + DomainFrMultiplicativeGenInv []byte + + // Infinity masks + InfinityA []byte + InfinityB []byte + NbInfinityA []byte + NbInfinityB []byte +} + +func (gnarkOutput *GnarkOutput) Bytes() []byte { + var gnarkOutputBytes bytes.Buffer + gnarkOutputBytes.Write(gnarkOutput.SolutionA) + gnarkOutputBytes.Write(gnarkOutput.SolutionB) + gnarkOutputBytes.Write(gnarkOutput.SolutionC) + gnarkOutputBytes.Write(gnarkOutput.WireValuesA) + gnarkOutputBytes.Write(gnarkOutput.WireValuesB) + gnarkOutputBytes.Write(gnarkOutput.WireValuesFiltered) + gnarkOutputBytes.Write(gnarkOutput.R) + gnarkOutputBytes.Write(gnarkOutput.S) + gnarkOutputBytes.Write(gnarkOutput.Kr) + gnarkOutputBytes.Write(gnarkOutput.PkA) + gnarkOutputBytes.Write(gnarkOutput.PkB) + gnarkOutputBytes.Write(gnarkOutput.PkZ) + gnarkOutputBytes.Write(gnarkOutput.PkK) + gnarkOutputBytes.Write(gnarkOutput.PkG2B) + gnarkOutputBytes.Write(gnarkOutput.PkG1Alpha) + gnarkOutputBytes.Write(gnarkOutput.PkG1Beta) + gnarkOutputBytes.Write(gnarkOutput.PkG1Delta) + gnarkOutputBytes.Write(gnarkOutput.PkG2Beta) + gnarkOutputBytes.Write(gnarkOutput.PkG2Delta) + gnarkOutputBytes.Write(gnarkOutput.DomainCardinality) + gnarkOutputBytes.Write(gnarkOutput.DomainCardinalityInv) + gnarkOutputBytes.Write(gnarkOutput.DomainGenerator) + gnarkOutputBytes.Write(gnarkOutput.DomainGeneratorInv) + gnarkOutputBytes.Write(gnarkOutput.DomainFrMultiplicativeGen) + gnarkOutputBytes.Write(gnarkOutput.DomainFrMultiplicativeGenInv) + gnarkOutputBytes.Write(gnarkOutput.InfinityA) + gnarkOutputBytes.Write(gnarkOutput.InfinityB) + gnarkOutputBytes.Write(gnarkOutput.NbInfinityA) + gnarkOutputBytes.Write(gnarkOutput.NbInfinityB) + return gnarkOutputBytes.Bytes() +} + +func serialProve(r1cs *cs.R1CS, pk *ProvingKey, fullWitness witness.Witness, opts ...backend.ProverOption) (*Proof, error) { + opt, err := backend.NewProverConfig(opts...) + if err != nil { + return nil, fmt.Errorf("new prover config: %w", err) + } + if opt.HashToFieldFn == nil { + opt.HashToFieldFn = hash_to_field.New([]byte(constraint.CommitmentDst)) + } + + log := logger.Logger().With().Str("curve", r1cs.CurveID().String()).Str("acceleration", "none"). + Int("nbConstraints", r1cs.GetNbConstraints()).Str("backend", "groth16").Logger() + + commitmentInfo := r1cs.CommitmentInfo.(constraint.Groth16Commitments) + + proof := &Proof{Commitments: make([]curve.G1Affine, len(commitmentInfo))} + + solverOpts := opt.SolverOpts[:len(opt.SolverOpts):len(opt.SolverOpts)] + + privateCommittedValues := make([][]fr.Element, len(commitmentInfo)) + + // override hints + bsb22ID := solver.GetHintID(fcs.Bsb22CommitmentComputePlaceholder) + solverOpts = append(solverOpts, solver.OverrideHint(bsb22ID, func(_ *big.Int, in []*big.Int, out []*big.Int) error { + i := int(in[0].Int64()) + in = in[1:] + privateCommittedValues[i] = make([]fr.Element, len(commitmentInfo[i].PrivateCommitted)) + hashed := in[:len(commitmentInfo[i].PublicAndCommitmentCommitted)] + committed := in[+len(hashed):] + for j, inJ := range committed { + privateCommittedValues[i][j].SetBigInt(inJ) + } + + var err error + if proof.Commitments[i], err = pk.CommitmentKeys[i].Commit(privateCommittedValues[i]); err != nil { + return err + } + + opt.HashToFieldFn.Write(constraint.SerializeCommitment(proof.Commitments[i].Marshal(), hashed, (fr.Bits-1)/8+1)) + hashBts := opt.HashToFieldFn.Sum(nil) + opt.HashToFieldFn.Reset() + nbBuf := fr.Bytes + if opt.HashToFieldFn.Size() < fr.Bytes { + nbBuf = opt.HashToFieldFn.Size() + } + var res fr.Element + res.SetBytes(hashBts[:nbBuf]) + res.BigInt(out[0]) + return nil + })) + + _solution, err := r1cs.Solve(fullWitness, solverOpts...) + if err != nil { + return nil, err + } + + solution := _solution.(*cs.R1CSSolution) + wireValues := []fr.Element(solution.W) + + start := time.Now() + poks := make([]curve.G1Affine, len(pk.CommitmentKeys)) + + for i := range pk.CommitmentKeys { + var err error + if poks[i], err = pk.CommitmentKeys[i].ProveKnowledge(privateCommittedValues[i]); err != nil { + return nil, err + } + } + // compute challenge for folding the PoKs from the commitments + commitmentsSerialized := make([]byte, fr.Bytes*len(commitmentInfo)) + for i := range commitmentInfo { + copy(commitmentsSerialized[fr.Bytes*i:], wireValues[commitmentInfo[i].CommitmentIndex].Marshal()) + } + challenge, err := fr.Hash(commitmentsSerialized, []byte("G16-BSB22"), 1) + if err != nil { + return nil, err + } + if _, err = proof.CommitmentPok.Fold(poks, challenge[0], ecc.MultiExpConfig{NbTasks: 1}); err != nil { + return nil, err + } + + // H (witness reduction / FFT part) + var h []fr.Element + { + h = computeH(solution.A, solution.B, solution.C, &pk.Domain) + solution.A = nil + solution.B = nil + solution.C = nil + } + + // we need to copy and filter the wireValues for each multi exp + // as pk.G1.A, pk.G1.B and pk.G2.B may have (a significant) number of point at infinity + var wireValuesA, wireValuesB []fr.Element + { + wireValuesA = make([]fr.Element, len(wireValues)-int(pk.NbInfinityA)) + for i, j := 0, 0; j < len(wireValuesA); i++ { + if pk.InfinityA[i] { + continue + } + wireValuesA[j] = wireValues[i] + j++ + } + } + { + wireValuesB = make([]fr.Element, len(wireValues)-int(pk.NbInfinityB)) + for i, j := 0, 0; j < len(wireValuesB); i++ { + if pk.InfinityB[i] { + continue + } + wireValuesB[j] = wireValues[i] + j++ + } + } + + // sample random r and s + var r, s big.Int + var _r, _s, _kr fr.Element + if _, err := _r.SetRandom(); err != nil { + return nil, err + } + if _, err := _s.SetRandom(); err != nil { + return nil, err + } + _kr.Mul(&_r, &_s).Neg(&_kr) + + _r.BigInt(&r) + _s.BigInt(&s) + + // computes r[δ], s[δ], kr[δ] + deltas := curve.BatchScalarMultiplicationG1(&pk.G1.Delta, []fr.Element{_r, _s, _kr}) + + var bs1, ar curve.G1Jac + + // computeBS1 + { + if _, err := bs1.MultiExp(pk.G1.B, wireValuesB, ecc.MultiExpConfig{NbTasks: 1}); err != nil { + return nil, err + } + bs1.AddMixed(&pk.G1.Beta) + bs1.AddMixed(&deltas[1]) + } + + // computeAR1 + { + if _, err := ar.MultiExp(pk.G1.A, wireValuesA, ecc.MultiExpConfig{NbTasks: 1}); err != nil { + return nil, err + } + ar.AddMixed(&pk.G1.Alpha) + ar.AddMixed(&deltas[0]) + proof.Ar.FromJacobian(&ar) + } + + // computeKRS + { + // we could NOT split the Krs multiExp in 2, and just append pk.G1.K and pk.G1.Z + // however, having similar lengths for our tasks helps with parallelism + + var krs, krs2, p1 curve.G1Jac + sizeH := int(pk.Domain.Cardinality - 1) // comes from the fact the deg(H)=(n-1)+(n-1)-n=n-2 + + _, err := krs2.MultiExp(pk.G1.Z, h[:sizeH], ecc.MultiExpConfig{NbTasks: 1}) + if err != nil { + return nil, err + } + + // filter the wire values if needed + // TODO Perf @Tabaie worst memory allocation offender + toRemove := commitmentInfo.GetPrivateCommitted() + toRemove = append(toRemove, commitmentInfo.CommitmentIndexes()) + _wireValues := filterHeap(wireValues[r1cs.GetNbPublicVariables():], r1cs.GetNbPublicVariables(), internal.ConcatAll(toRemove...)) + + if _, err = krs.MultiExp(pk.G1.K, _wireValues, ecc.MultiExpConfig{NbTasks: 1}); err != nil { + return nil, err + } + krs.AddMixed(&deltas[2]) + + krs.AddAssign(&krs2) + + p1.ScalarMultiplication(&ar, &s) + krs.AddAssign(&p1) + + p1.ScalarMultiplication(&bs1, &r) + krs.AddAssign(&p1) + + proof.Krs.FromJacobian(&krs) + } + + // computeBS2 + computeBS2 := func() error { + // Bs2 (1 multi exp G2 - size = len(wires)) + var Bs, deltaS curve.G2Jac + + if _, err := Bs.MultiExp(pk.G2.B, wireValuesB, ecc.MultiExpConfig{NbTasks: 1}); err != nil { + return err + } + + deltaS.FromAffine(&pk.G2.Delta) + deltaS.ScalarMultiplication(&deltaS, &s) + Bs.AddAssign(&deltaS) + Bs.AddMixed(&pk.G2.Beta) + + proof.Bs.FromJacobian(&Bs) + return nil + } + + // wait for FFT to end, as it uses all our CPUs + + // schedule our proof part computations + if err := computeBS2(); err != nil { + return nil, err + } + + // wait for all parts of the proof to be computed. + + log.Debug().Dur("took", time.Since(start)).Msg("prover done") + + return proof, nil +} + // if len(toRemove) == 0, returns slice // else, returns a new slice without the indexes in toRemove. The first value in the slice is taken as indexes as sliceFirstIndex // this assumes len(slice) > len(toRemove) diff --git a/backend/groth16/bn254/setup.go b/backend/groth16/bn254/setup.go index 8840eb57..bb0212c5 100644 --- a/backend/groth16/bn254/setup.go +++ b/backend/groth16/bn254/setup.go @@ -7,6 +7,9 @@ package groth16 import ( "errors" + "math/big" + "math/bits" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bn254" "github.com/consensys/gnark-crypto/ecc/bn254/fr" @@ -15,8 +18,6 @@ import ( "github.com/consensys/gnark/backend/groth16/internal" "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bn254" - "math/big" - "math/bits" ) // ProvingKey is used by a Groth16 prover to encode a proof of a statement diff --git a/backend/groth16/bn254/verify.go b/backend/groth16/bn254/verify.go index 1b8cd2ea..f293b314 100644 --- a/backend/groth16/bn254/verify.go +++ b/backend/groth16/bn254/verify.go @@ -10,13 +10,14 @@ import ( "crypto/sha256" "errors" "fmt" - "github.com/consensys/gnark-crypto/ecc/bn254/fp" - "golang.org/x/crypto/sha3" "io" "math/big" "text/template" "time" + "github.com/consensys/gnark-crypto/ecc/bn254/fp" + "golang.org/x/crypto/sha3" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bn254" "github.com/consensys/gnark-crypto/ecc/bn254/fr" diff --git a/backend/groth16/bw6-633/mpcsetup/marshal.go b/backend/groth16/bw6-633/mpcsetup/marshal.go index 5560b38d..82ef307f 100644 --- a/backend/groth16/bw6-633/mpcsetup/marshal.go +++ b/backend/groth16/bw6-633/mpcsetup/marshal.go @@ -7,11 +7,12 @@ package mpcsetup import ( "encoding/binary" + "io" + curve "github.com/consensys/gnark-crypto/ecc/bw6-633" "github.com/consensys/gnark-crypto/ecc/bw6-633/mpcsetup" "github.com/consensys/gnark/internal/utils" gIo "github.com/consensys/gnark/io" - "io" ) // WriteTo implements io.WriterTo diff --git a/backend/groth16/bw6-633/mpcsetup/phase1.go b/backend/groth16/bw6-633/mpcsetup/phase1.go index bd6ef4cb..3a4eedaf 100644 --- a/backend/groth16/bw6-633/mpcsetup/phase1.go +++ b/backend/groth16/bw6-633/mpcsetup/phase1.go @@ -10,11 +10,12 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bw6-633" "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" "github.com/consensys/gnark-crypto/ecc/bw6-633/mpcsetup" - "math/big" ) // SrsCommons are the circuit-independent components of the Groth16 SRS, @@ -213,10 +214,10 @@ func (p *Phase1) Verify(next *Phase1) error { } return mpcsetup.SameRatioMany( - p.parameters.G1.Tau, - p.parameters.G2.Tau, - p.parameters.G1.AlphaTau, - p.parameters.G1.BetaTau, + next.parameters.G1.Tau, + next.parameters.G2.Tau, + next.parameters.G1.AlphaTau, + next.parameters.G1.BetaTau, ) } diff --git a/backend/groth16/bw6-633/mpcsetup/phase2.go b/backend/groth16/bw6-633/mpcsetup/phase2.go index e165646c..a386d459 100644 --- a/backend/groth16/bw6-633/mpcsetup/phase2.go +++ b/backend/groth16/bw6-633/mpcsetup/phase2.go @@ -10,6 +10,9 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "slices" + curve "github.com/consensys/gnark-crypto/ecc/bw6-633" "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" "github.com/consensys/gnark-crypto/ecc/bw6-633/mpcsetup" @@ -18,8 +21,6 @@ import ( "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bw6-633" "github.com/consensys/gnark/internal/utils" - "math/big" - "slices" ) // Phase2Evaluations components of the circuit keys diff --git a/backend/groth16/bw6-633/mpcsetup/setup_test.go b/backend/groth16/bw6-633/mpcsetup/setup_test.go index 284f44f9..f5e10103 100644 --- a/backend/groth16/bw6-633/mpcsetup/setup_test.go +++ b/backend/groth16/bw6-633/mpcsetup/setup_test.go @@ -8,17 +8,18 @@ package mpcsetup import ( "bytes" "fmt" - "github.com/consensys/gnark-crypto/ecc" - curve "github.com/consensys/gnark-crypto/ecc/bw6-633" - "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" - groth16Impl "github.com/consensys/gnark/backend/groth16/bw6-633" - cs "github.com/consensys/gnark/constraint/bw6-633" "io" "math/big" "slices" "sync" "testing" + "github.com/consensys/gnark-crypto/ecc" + curve "github.com/consensys/gnark-crypto/ecc/bw6-633" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" + groth16Impl "github.com/consensys/gnark/backend/groth16/bw6-633" + cs "github.com/consensys/gnark/constraint/bw6-633" + "github.com/consensys/gnark/backend/groth16" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" diff --git a/backend/groth16/bw6-633/prove.go b/backend/groth16/bw6-633/prove.go index 3783ccf7..6736c40d 100644 --- a/backend/groth16/bw6-633/prove.go +++ b/backend/groth16/bw6-633/prove.go @@ -7,6 +7,10 @@ package groth16 import ( "fmt" + "math/big" + "runtime" + "time" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bw6-633" "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" @@ -20,9 +24,6 @@ import ( "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/internal/utils" "github.com/consensys/gnark/logger" - "math/big" - "runtime" - "time" fcs "github.com/consensys/gnark/frontend/cs" ) diff --git a/backend/groth16/bw6-633/setup.go b/backend/groth16/bw6-633/setup.go index 5d163018..0fe030bd 100644 --- a/backend/groth16/bw6-633/setup.go +++ b/backend/groth16/bw6-633/setup.go @@ -7,6 +7,9 @@ package groth16 import ( "errors" + "math/big" + "math/bits" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bw6-633" "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" @@ -15,8 +18,6 @@ import ( "github.com/consensys/gnark/backend/groth16/internal" "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bw6-633" - "math/big" - "math/bits" ) // ProvingKey is used by a Groth16 prover to encode a proof of a statement diff --git a/backend/groth16/bw6-761/mpcsetup/marshal.go b/backend/groth16/bw6-761/mpcsetup/marshal.go index 47c3e866..7f8f70b3 100644 --- a/backend/groth16/bw6-761/mpcsetup/marshal.go +++ b/backend/groth16/bw6-761/mpcsetup/marshal.go @@ -7,11 +7,12 @@ package mpcsetup import ( "encoding/binary" + "io" + curve "github.com/consensys/gnark-crypto/ecc/bw6-761" "github.com/consensys/gnark-crypto/ecc/bw6-761/mpcsetup" "github.com/consensys/gnark/internal/utils" gIo "github.com/consensys/gnark/io" - "io" ) // WriteTo implements io.WriterTo diff --git a/backend/groth16/bw6-761/mpcsetup/phase1.go b/backend/groth16/bw6-761/mpcsetup/phase1.go index c63b4b9b..d9594b0f 100644 --- a/backend/groth16/bw6-761/mpcsetup/phase1.go +++ b/backend/groth16/bw6-761/mpcsetup/phase1.go @@ -10,11 +10,12 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bw6-761" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" "github.com/consensys/gnark-crypto/ecc/bw6-761/mpcsetup" - "math/big" ) // SrsCommons are the circuit-independent components of the Groth16 SRS, @@ -213,10 +214,10 @@ func (p *Phase1) Verify(next *Phase1) error { } return mpcsetup.SameRatioMany( - p.parameters.G1.Tau, - p.parameters.G2.Tau, - p.parameters.G1.AlphaTau, - p.parameters.G1.BetaTau, + next.parameters.G1.Tau, + next.parameters.G2.Tau, + next.parameters.G1.AlphaTau, + next.parameters.G1.BetaTau, ) } diff --git a/backend/groth16/bw6-761/mpcsetup/phase2.go b/backend/groth16/bw6-761/mpcsetup/phase2.go index 70341c59..36b99d96 100644 --- a/backend/groth16/bw6-761/mpcsetup/phase2.go +++ b/backend/groth16/bw6-761/mpcsetup/phase2.go @@ -10,6 +10,9 @@ import ( "crypto/sha256" "errors" "fmt" + "math/big" + "slices" + curve "github.com/consensys/gnark-crypto/ecc/bw6-761" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" "github.com/consensys/gnark-crypto/ecc/bw6-761/mpcsetup" @@ -18,8 +21,6 @@ import ( "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bw6-761" "github.com/consensys/gnark/internal/utils" - "math/big" - "slices" ) // Phase2Evaluations components of the circuit keys diff --git a/backend/groth16/bw6-761/mpcsetup/setup_test.go b/backend/groth16/bw6-761/mpcsetup/setup_test.go index ce16700c..c7c70da1 100644 --- a/backend/groth16/bw6-761/mpcsetup/setup_test.go +++ b/backend/groth16/bw6-761/mpcsetup/setup_test.go @@ -8,17 +8,18 @@ package mpcsetup import ( "bytes" "fmt" - "github.com/consensys/gnark-crypto/ecc" - curve "github.com/consensys/gnark-crypto/ecc/bw6-761" - "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" - groth16Impl "github.com/consensys/gnark/backend/groth16/bw6-761" - cs "github.com/consensys/gnark/constraint/bw6-761" "io" "math/big" "slices" "sync" "testing" + "github.com/consensys/gnark-crypto/ecc" + curve "github.com/consensys/gnark-crypto/ecc/bw6-761" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + groth16Impl "github.com/consensys/gnark/backend/groth16/bw6-761" + cs "github.com/consensys/gnark/constraint/bw6-761" + "github.com/consensys/gnark/backend/groth16" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" diff --git a/backend/groth16/bw6-761/prove.go b/backend/groth16/bw6-761/prove.go index d4705bb2..4475ffd2 100644 --- a/backend/groth16/bw6-761/prove.go +++ b/backend/groth16/bw6-761/prove.go @@ -7,6 +7,10 @@ package groth16 import ( "fmt" + "math/big" + "runtime" + "time" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bw6-761" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" @@ -20,9 +24,6 @@ import ( "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/internal/utils" "github.com/consensys/gnark/logger" - "math/big" - "runtime" - "time" fcs "github.com/consensys/gnark/frontend/cs" ) diff --git a/backend/groth16/bw6-761/setup.go b/backend/groth16/bw6-761/setup.go index e8c0ed39..e9488c0b 100644 --- a/backend/groth16/bw6-761/setup.go +++ b/backend/groth16/bw6-761/setup.go @@ -7,6 +7,9 @@ package groth16 import ( "errors" + "math/big" + "math/bits" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bw6-761" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" @@ -15,8 +18,6 @@ import ( "github.com/consensys/gnark/backend/groth16/internal" "github.com/consensys/gnark/constraint" cs "github.com/consensys/gnark/constraint/bw6-761" - "math/big" - "math/bits" ) // ProvingKey is used by a Groth16 prover to encode a proof of a statement diff --git a/backend/groth16/groth16.go b/backend/groth16/groth16.go index 3c4d1b56..98bb10f1 100644 --- a/backend/groth16/groth16.go +++ b/backend/groth16/groth16.go @@ -187,9 +187,6 @@ func Prove(r1cs constraint.ConstraintSystem, pk ProvingKey, fullWitness witness. return groth16_bls12381.Prove(_r1cs, pk.(*groth16_bls12381.ProvingKey), fullWitness, opts...) case *cs_bn254.R1CS: - if icicle_bn254.HasIcicle { - return icicle_bn254.Prove(_r1cs, pk.(*icicle_bn254.ProvingKey), fullWitness, opts...) - } return groth16_bn254.Prove(_r1cs, pk.(*groth16_bn254.ProvingKey), fullWitness, opts...) case *cs_bw6761.R1CS: @@ -209,6 +206,15 @@ func Prove(r1cs constraint.ConstraintSystem, pk ProvingKey, fullWitness witness. } } +func ExtractIntermediateData(r1cs constraint.ConstraintSystem, pk ProvingKey, fullWitness witness.Witness, opts ...backend.ProverOption) (*groth16_bn254.GnarkOutput, error) { + switch _r1cs := r1cs.(type) { + case *cs_bn254.R1CS: + return groth16_bn254.ExtractIntermediateData(_r1cs, pk.(*groth16_bn254.ProvingKey), fullWitness, opts...) + default: + panic("unrecognized R1CS curve type") + } +} + // Setup runs groth16.Setup with provided R1CS and outputs a key pair associated with the circuit. // // Note that careful consideration must be given to this step in a production environment. diff --git a/backend/plonk/bls12-377/marshal.go b/backend/plonk/bls12-377/marshal.go index 45fc6ea3..347d7b0e 100644 --- a/backend/plonk/bls12-377/marshal.go +++ b/backend/plonk/bls12-377/marshal.go @@ -8,8 +8,9 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bls12-377" - "github.com/consensys/gnark-crypto/ecc/bls12-377/kzg" "io" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/kzg" ) // WriteRawTo writes binary encoding of Proof to w without point compression diff --git a/backend/plonk/bls12-377/marshal_test.go b/backend/plonk/bls12-377/marshal_test.go index fdfc7dae..60a38188 100644 --- a/backend/plonk/bls12-377/marshal_test.go +++ b/backend/plonk/bls12-377/marshal_test.go @@ -8,12 +8,13 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bls12-377" - "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" - "github.com/consensys/gnark/io" "math/big" "math/rand" "testing" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark/io" + "github.com/stretchr/testify/assert" ) diff --git a/backend/plonk/bls12-377/setup.go b/backend/plonk/bls12-377/setup.go index 8e469d1f..53eca1c3 100644 --- a/backend/plonk/bls12-377/setup.go +++ b/backend/plonk/bls12-377/setup.go @@ -7,6 +7,7 @@ package plonk import ( "fmt" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/fft" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/iop" diff --git a/backend/plonk/bls12-377/verify.go b/backend/plonk/bls12-377/verify.go index 571d4468..efa9ea1e 100644 --- a/backend/plonk/bls12-377/verify.go +++ b/backend/plonk/bls12-377/verify.go @@ -11,9 +11,10 @@ import ( "io" "math/big" - "github.com/consensys/gnark/backend/solidity" "time" + "github.com/consensys/gnark/backend/solidity" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls12-377" diff --git a/backend/plonk/bls12-381/marshal.go b/backend/plonk/bls12-381/marshal.go index 18245c73..33883f22 100644 --- a/backend/plonk/bls12-381/marshal.go +++ b/backend/plonk/bls12-381/marshal.go @@ -8,8 +8,9 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bls12-381" - "github.com/consensys/gnark-crypto/ecc/bls12-381/kzg" "io" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/kzg" ) // WriteRawTo writes binary encoding of Proof to w without point compression diff --git a/backend/plonk/bls12-381/marshal_test.go b/backend/plonk/bls12-381/marshal_test.go index 67a83861..ed07148d 100644 --- a/backend/plonk/bls12-381/marshal_test.go +++ b/backend/plonk/bls12-381/marshal_test.go @@ -8,12 +8,13 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bls12-381" - "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" - "github.com/consensys/gnark/io" "math/big" "math/rand" "testing" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark/io" + "github.com/stretchr/testify/assert" ) diff --git a/backend/plonk/bls12-381/setup.go b/backend/plonk/bls12-381/setup.go index 7bca6364..4a1973ac 100644 --- a/backend/plonk/bls12-381/setup.go +++ b/backend/plonk/bls12-381/setup.go @@ -7,6 +7,7 @@ package plonk import ( "fmt" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/fft" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/iop" diff --git a/backend/plonk/bls12-381/verify.go b/backend/plonk/bls12-381/verify.go index 307e5de5..edbbfa86 100644 --- a/backend/plonk/bls12-381/verify.go +++ b/backend/plonk/bls12-381/verify.go @@ -11,9 +11,10 @@ import ( "io" "math/big" - "github.com/consensys/gnark/backend/solidity" "time" + "github.com/consensys/gnark/backend/solidity" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls12-381" diff --git a/backend/plonk/bls24-315/marshal.go b/backend/plonk/bls24-315/marshal.go index 1369d32b..e52383ad 100644 --- a/backend/plonk/bls24-315/marshal.go +++ b/backend/plonk/bls24-315/marshal.go @@ -8,8 +8,9 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bls24-315" - "github.com/consensys/gnark-crypto/ecc/bls24-315/kzg" "io" + + "github.com/consensys/gnark-crypto/ecc/bls24-315/kzg" ) // WriteRawTo writes binary encoding of Proof to w without point compression diff --git a/backend/plonk/bls24-315/marshal_test.go b/backend/plonk/bls24-315/marshal_test.go index 24071316..6e7d0ca8 100644 --- a/backend/plonk/bls24-315/marshal_test.go +++ b/backend/plonk/bls24-315/marshal_test.go @@ -8,12 +8,13 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bls24-315" - "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" - "github.com/consensys/gnark/io" "math/big" "math/rand" "testing" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" + "github.com/consensys/gnark/io" + "github.com/stretchr/testify/assert" ) diff --git a/backend/plonk/bls24-315/setup.go b/backend/plonk/bls24-315/setup.go index adc8b7fe..564b5cc5 100644 --- a/backend/plonk/bls24-315/setup.go +++ b/backend/plonk/bls24-315/setup.go @@ -7,6 +7,7 @@ package plonk import ( "fmt" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/fft" "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/iop" diff --git a/backend/plonk/bls24-315/verify.go b/backend/plonk/bls24-315/verify.go index e4346b76..cff8efb8 100644 --- a/backend/plonk/bls24-315/verify.go +++ b/backend/plonk/bls24-315/verify.go @@ -11,9 +11,10 @@ import ( "io" "math/big" - "github.com/consensys/gnark/backend/solidity" "time" + "github.com/consensys/gnark/backend/solidity" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls24-315" diff --git a/backend/plonk/bls24-317/marshal.go b/backend/plonk/bls24-317/marshal.go index e8bb8699..13c84b2e 100644 --- a/backend/plonk/bls24-317/marshal.go +++ b/backend/plonk/bls24-317/marshal.go @@ -8,8 +8,9 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bls24-317" - "github.com/consensys/gnark-crypto/ecc/bls24-317/kzg" "io" + + "github.com/consensys/gnark-crypto/ecc/bls24-317/kzg" ) // WriteRawTo writes binary encoding of Proof to w without point compression diff --git a/backend/plonk/bls24-317/marshal_test.go b/backend/plonk/bls24-317/marshal_test.go index 78ba1388..bac38f89 100644 --- a/backend/plonk/bls24-317/marshal_test.go +++ b/backend/plonk/bls24-317/marshal_test.go @@ -8,12 +8,13 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bls24-317" - "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" - "github.com/consensys/gnark/io" "math/big" "math/rand" "testing" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" + "github.com/consensys/gnark/io" + "github.com/stretchr/testify/assert" ) diff --git a/backend/plonk/bls24-317/setup.go b/backend/plonk/bls24-317/setup.go index ae716c0b..5db0d82f 100644 --- a/backend/plonk/bls24-317/setup.go +++ b/backend/plonk/bls24-317/setup.go @@ -7,6 +7,7 @@ package plonk import ( "fmt" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/fft" "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/iop" diff --git a/backend/plonk/bls24-317/verify.go b/backend/plonk/bls24-317/verify.go index b92d4a6f..04e440e2 100644 --- a/backend/plonk/bls24-317/verify.go +++ b/backend/plonk/bls24-317/verify.go @@ -11,9 +11,10 @@ import ( "io" "math/big" - "github.com/consensys/gnark/backend/solidity" "time" + "github.com/consensys/gnark/backend/solidity" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bls24-317" diff --git a/backend/plonk/bn254/marshal.go b/backend/plonk/bn254/marshal.go index a5f41644..a5d8ffeb 100644 --- a/backend/plonk/bn254/marshal.go +++ b/backend/plonk/bn254/marshal.go @@ -8,8 +8,9 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bn254" - "github.com/consensys/gnark-crypto/ecc/bn254/kzg" "io" + + "github.com/consensys/gnark-crypto/ecc/bn254/kzg" ) // WriteRawTo writes binary encoding of Proof to w without point compression diff --git a/backend/plonk/bn254/marshal_test.go b/backend/plonk/bn254/marshal_test.go index 3c01ae3d..cab4b0e3 100644 --- a/backend/plonk/bn254/marshal_test.go +++ b/backend/plonk/bn254/marshal_test.go @@ -8,12 +8,13 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bn254" - "github.com/consensys/gnark-crypto/ecc/bn254/fr" - "github.com/consensys/gnark/io" "math/big" "math/rand" "testing" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark/io" + "github.com/stretchr/testify/assert" ) diff --git a/backend/plonk/bn254/setup.go b/backend/plonk/bn254/setup.go index 6324c432..f3c40fb4 100644 --- a/backend/plonk/bn254/setup.go +++ b/backend/plonk/bn254/setup.go @@ -7,6 +7,7 @@ package plonk import ( "fmt" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" "github.com/consensys/gnark-crypto/ecc/bn254/fr/fft" "github.com/consensys/gnark-crypto/ecc/bn254/fr/iop" diff --git a/backend/plonk/bn254/verify.go b/backend/plonk/bn254/verify.go index 4e39a312..01dbcdbe 100644 --- a/backend/plonk/bn254/verify.go +++ b/backend/plonk/bn254/verify.go @@ -8,12 +8,13 @@ package plonk import ( "errors" "fmt" - "github.com/consensys/gnark/backend/solidity" "io" "math/big" "text/template" "time" + "github.com/consensys/gnark/backend/solidity" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bn254" diff --git a/backend/plonk/bw6-633/marshal.go b/backend/plonk/bw6-633/marshal.go index 1a527dcd..a21a5e3d 100644 --- a/backend/plonk/bw6-633/marshal.go +++ b/backend/plonk/bw6-633/marshal.go @@ -8,8 +8,9 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bw6-633" - "github.com/consensys/gnark-crypto/ecc/bw6-633/kzg" "io" + + "github.com/consensys/gnark-crypto/ecc/bw6-633/kzg" ) // WriteRawTo writes binary encoding of Proof to w without point compression diff --git a/backend/plonk/bw6-633/marshal_test.go b/backend/plonk/bw6-633/marshal_test.go index 9f1c7a0e..4e299cbf 100644 --- a/backend/plonk/bw6-633/marshal_test.go +++ b/backend/plonk/bw6-633/marshal_test.go @@ -8,12 +8,13 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bw6-633" - "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" - "github.com/consensys/gnark/io" "math/big" "math/rand" "testing" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" + "github.com/consensys/gnark/io" + "github.com/stretchr/testify/assert" ) diff --git a/backend/plonk/bw6-633/setup.go b/backend/plonk/bw6-633/setup.go index 3cf74bf3..a34b63cc 100644 --- a/backend/plonk/bw6-633/setup.go +++ b/backend/plonk/bw6-633/setup.go @@ -7,6 +7,7 @@ package plonk import ( "fmt" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/fft" "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/iop" diff --git a/backend/plonk/bw6-633/verify.go b/backend/plonk/bw6-633/verify.go index 9d7837ed..3ebd30a9 100644 --- a/backend/plonk/bw6-633/verify.go +++ b/backend/plonk/bw6-633/verify.go @@ -11,9 +11,10 @@ import ( "io" "math/big" - "github.com/consensys/gnark/backend/solidity" "time" + "github.com/consensys/gnark/backend/solidity" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bw6-633" diff --git a/backend/plonk/bw6-761/marshal.go b/backend/plonk/bw6-761/marshal.go index 737f6012..c9608755 100644 --- a/backend/plonk/bw6-761/marshal.go +++ b/backend/plonk/bw6-761/marshal.go @@ -8,8 +8,9 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bw6-761" - "github.com/consensys/gnark-crypto/ecc/bw6-761/kzg" "io" + + "github.com/consensys/gnark-crypto/ecc/bw6-761/kzg" ) // WriteRawTo writes binary encoding of Proof to w without point compression diff --git a/backend/plonk/bw6-761/marshal_test.go b/backend/plonk/bw6-761/marshal_test.go index b2be1091..456df6bc 100644 --- a/backend/plonk/bw6-761/marshal_test.go +++ b/backend/plonk/bw6-761/marshal_test.go @@ -8,12 +8,13 @@ package plonk import ( curve "github.com/consensys/gnark-crypto/ecc/bw6-761" - "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" - "github.com/consensys/gnark/io" "math/big" "math/rand" "testing" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + "github.com/consensys/gnark/io" + "github.com/stretchr/testify/assert" ) diff --git a/backend/plonk/bw6-761/setup.go b/backend/plonk/bw6-761/setup.go index 8e51e234..ba2baa47 100644 --- a/backend/plonk/bw6-761/setup.go +++ b/backend/plonk/bw6-761/setup.go @@ -7,6 +7,7 @@ package plonk import ( "fmt" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/fft" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/iop" diff --git a/backend/plonk/bw6-761/verify.go b/backend/plonk/bw6-761/verify.go index 213f0a5d..dc6bb8c7 100644 --- a/backend/plonk/bw6-761/verify.go +++ b/backend/plonk/bw6-761/verify.go @@ -11,9 +11,10 @@ import ( "io" "math/big" - "github.com/consensys/gnark/backend/solidity" "time" + "github.com/consensys/gnark/backend/solidity" + "github.com/consensys/gnark-crypto/ecc" curve "github.com/consensys/gnark-crypto/ecc/bw6-761" diff --git a/backend/witness/vector.go b/backend/witness/vector.go index 248e293a..f6453a2c 100644 --- a/backend/witness/vector.go +++ b/backend/witness/vector.go @@ -13,7 +13,9 @@ import ( fr_bn254 "github.com/consensys/gnark-crypto/ecc/bn254/fr" fr_bw6633 "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" fr_bw6761 "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" - "github.com/consensys/gnark/internal/tinyfield" + "github.com/consensys/gnark-crypto/field/babybear" + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark/internal/smallfields/tinyfield" "github.com/consensys/gnark/internal/utils" ) @@ -37,9 +39,14 @@ func newVector(field *big.Int, size int) (any, error) { default: if field.Cmp(tinyfield.Modulus()) == 0 { return make(tinyfield.Vector, size), nil - } else { - return nil, errors.New("unsupported modulus") } + if field.Cmp(babybear.Modulus()) == 0 { + return make(babybear.Vector, size), nil + } + if field.Cmp(koalabear.Modulus()) == 0 { + return make(koalabear.Vector, size), nil + } + return nil, errors.New("unsupported modulus") } } @@ -77,6 +84,14 @@ func newFrom(from any, n int) (any, error) { a := make(tinyfield.Vector, n) copy(a, wt) return a, nil + case babybear.Vector: + a := make(babybear.Vector, n) + copy(a, wt) + return a, nil + case koalabear.Vector: + a := make(koalabear.Vector, n) + copy(a, wt) + return a, nil default: return nil, errors.New("unsupported modulus") } @@ -100,6 +115,10 @@ func leafType(v any) reflect.Type { return reflect.TypeOf(fr_bw6633.Element{}) case tinyfield.Vector: return reflect.TypeOf(tinyfield.Element{}) + case babybear.Vector: + return reflect.TypeOf(babybear.Element{}) + case koalabear.Vector: + return reflect.TypeOf(koalabear.Element{}) default: panic("invalid input") } @@ -155,6 +174,18 @@ func set(v any, index int, value any) error { } _, err := pv[index].SetInterface(value) return err + case babybear.Vector: + if index >= len(pv) { + return errors.New("out of bounds") + } + _, err := pv[index].SetInterface(value) + return err + case koalabear.Vector: + if index >= len(pv) { + return errors.New("out of bounds") + } + _, err := pv[index].SetInterface(value) + return err default: panic("invalid input") } @@ -219,6 +250,20 @@ func iterate(v any) chan any { } close(chValues) }() + case babybear.Vector: + go func() { + for i := 0; i < len(pv); i++ { + chValues <- &(pv)[i] + } + close(chValues) + }() + case koalabear.Vector: + go func() { + for i := 0; i < len(pv); i++ { + chValues <- &(pv)[i] + } + close(chValues) + }() default: panic("invalid input") } @@ -243,6 +288,10 @@ func resize(v any, n int) any { return make(fr_bw6633.Vector, n) case tinyfield.Vector: return make(tinyfield.Vector, n) + case babybear.Vector: + return make(babybear.Vector, n) + case koalabear.Vector: + return make(koalabear.Vector, n) default: panic("invalid input") } diff --git a/backend/witness/witness.go b/backend/witness/witness.go index 0e81f124..1295eaf9 100644 --- a/backend/witness/witness.go +++ b/backend/witness/witness.go @@ -54,9 +54,11 @@ import ( fr_bn254 "github.com/consensys/gnark-crypto/ecc/bn254/fr" fr_bw6633 "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" fr_bw6761 "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + "github.com/consensys/gnark-crypto/field/babybear" + "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark/debug" "github.com/consensys/gnark/frontend/schema" - "github.com/consensys/gnark/internal/tinyfield" + "github.com/consensys/gnark/internal/smallfields/tinyfield" ) var ErrInvalidWitness = errors.New("invalid witness") @@ -189,6 +191,10 @@ func (w *witness) WriteTo(wr io.Writer) (n int64, err error) { m, err = t.WriteTo(wr) case tinyfield.Vector: m, err = t.WriteTo(wr) + case babybear.Vector: + m, err = t.WriteTo(wr) + case koalabear.Vector: + m, err = t.WriteTo(wr) default: panic("invalid input") } @@ -235,6 +241,12 @@ func (w *witness) ReadFrom(r io.Reader) (n int64, err error) { case tinyfield.Vector: m, err = t.ReadFrom(r) w.vector = t + case babybear.Vector: + m, err = t.ReadFrom(r) + w.vector = t + case koalabear.Vector: + m, err = t.ReadFrom(r) + w.vector = t default: panic("invalid input") } @@ -274,7 +286,7 @@ func (w *witness) ToJSON(s *schema.Schema) ([]byte, error) { instance := s.Instantiate(typ) chValues := w.iterate() - if _, err := schema.Walk(instance, typ, func(field schema.LeafInfo, tValue reflect.Value) error { + if _, err := schema.Walk(s.Field, instance, typ, func(field schema.LeafInfo, tValue reflect.Value) error { if field.Visibility == schema.Public { v := <-chValues tValue.Set(reflect.ValueOf(v)) @@ -286,7 +298,7 @@ func (w *witness) ToJSON(s *schema.Schema) ([]byte, error) { if w.nbSecret != 0 { // secret part. - if _, err := schema.Walk(instance, typ, func(field schema.LeafInfo, tValue reflect.Value) error { + if _, err := schema.Walk(s.Field, instance, typ, func(field schema.LeafInfo, tValue reflect.Value) error { if field.Visibility == schema.Secret { v := <-chValues tValue.Set(reflect.ValueOf(v)) @@ -328,7 +340,7 @@ func (w *witness) FromJSON(s *schema.Schema, data []byte) error { // collect all public values; if any are missing, no point going further. publicValues := make([]any, 0, s.NbPublic) - if _, err := schema.Walk(instance, ptrTyp, func(leaf schema.LeafInfo, tValue reflect.Value) error { + if _, err := schema.Walk(s.Field, instance, ptrTyp, func(leaf schema.LeafInfo, tValue reflect.Value) error { if leaf.Visibility == schema.Public { if tValue.IsNil() { return missingAssignment(leaf.FullName()) @@ -344,7 +356,7 @@ func (w *witness) FromJSON(s *schema.Schema, data []byte) error { // collect all secret values; if any are missing, we just deal with the public part. secretValues := make([]any, 0, s.NbSecret) publicOnly := false - if _, err := schema.Walk(instance, ptrTyp, func(leaf schema.LeafInfo, tValue reflect.Value) error { + if _, err := schema.Walk(s.Field, instance, ptrTyp, func(leaf schema.LeafInfo, tValue reflect.Value) error { if leaf.Visibility == schema.Secret { if tValue.IsNil() { return missingAssignment(leaf.FullName()) diff --git a/backend/witness/witness_test.go b/backend/witness/witness_test.go index 8666c734..bcbde538 100644 --- a/backend/witness/witness_test.go +++ b/backend/witness/witness_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "math/big" "reflect" "testing" @@ -11,6 +12,7 @@ import ( "github.com/consensys/gnark-crypto/ecc/bn254/fr" "github.com/consensys/gnark/backend/witness" "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/smallfields/tinyfield" "github.com/consensys/gnark/io" "github.com/stretchr/testify/require" ) @@ -49,7 +51,7 @@ func ExampleWitness() { // complex circuit structures well. // first get the circuit expected schema - schema, _ := frontend.NewSchema(assignment) + schema, _ := frontend.NewSchema(ecc.BN254.ScalarField(), assignment) ret, _ := reconstructed.ToJSON(schema) var b bytes.Buffer @@ -134,7 +136,7 @@ func roundTripMarshalJSON(assert *require.Assertions, assignment circuit, public w, err := frontend.NewWitness(&assignment, ecc.BN254.ScalarField(), opts...) assert.NoError(err) - s, err := frontend.NewSchema(&assignment) + s, err := frontend.NewSchema(ecc.BN254.ScalarField(), &assignment) assert.NoError(err) // serialize the vector to JSON @@ -155,9 +157,11 @@ type initableVariable struct { Val []frontend.Variable } -func (iv *initableVariable) GnarkInitHook() { - if iv.Val == nil { +func (iv *initableVariable) Initialize(field *big.Int) { + if field.Cmp(ecc.BN254.ScalarField()) == 0 { iv.Val = []frontend.Variable{1, 2} // need to init value as are assigning to witness + } else { + iv.Val = []frontend.Variable{1, 2, 3} } } @@ -180,4 +184,11 @@ func TestVariableInitHook(t *testing.T) { fw, ok := w.Vector().(fr.Vector) assert.True(ok) assert.Len(fw, 10, "invalid length") + + // check that we call field-specific init + w2, err := frontend.NewWitness(assignment, tinyfield.Modulus()) + assert.NoError(err) + fw2, ok := w2.Vector().(tinyfield.Vector) + assert.True(ok) + assert.Len(fw2, 15, "invalid length") } diff --git a/constraint/babybear/coeff.go b/constraint/babybear/coeff.go new file mode 100644 index 00000000..0ca53057 --- /dev/null +++ b/constraint/babybear/coeff.go @@ -0,0 +1,220 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package cs + +import ( + "encoding/binary" + "errors" + "math/big" + + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/internal/utils" + + fr "github.com/consensys/gnark-crypto/field/babybear" +) + +// CoeffTable ensure we store unique coefficients in the constraint system +type CoeffTable struct { + Coefficients []fr.Element + mCoeffs map[fr.Element]uint32 // maps coefficient to coeffID +} + +func newCoeffTable(capacity int) CoeffTable { + r := CoeffTable{ + Coefficients: make([]fr.Element, 5, 5+capacity), + mCoeffs: make(map[fr.Element]uint32, capacity), + } + + r.Coefficients[constraint.CoeffIdZero].SetUint64(0) + r.Coefficients[constraint.CoeffIdOne].SetOne() + r.Coefficients[constraint.CoeffIdTwo].SetUint64(2) + r.Coefficients[constraint.CoeffIdMinusOne].SetInt64(-1) + r.Coefficients[constraint.CoeffIdMinusTwo].SetInt64(-2) + + return r + +} + +func (ct *CoeffTable) toBytes() []byte { + buf := make([]byte, 0, 8+len(ct.Coefficients)*fr.Bytes) + ctLen := uint64(len(ct.Coefficients)) + + buf = binary.LittleEndian.AppendUint64(buf, ctLen) + for _, c := range ct.Coefficients { + for _, w := range c { + buf = binary.LittleEndian.AppendUint32(buf, w) + } + } + + return buf +} + +func (ct *CoeffTable) fromBytes(buf []byte) error { + if len(buf) < 8 { + return errors.New("invalid buffer size") + } + ctLen := binary.LittleEndian.Uint64(buf[:8]) + buf = buf[8:] + + if uint64(len(buf)) < ctLen*fr.Bytes { + return errors.New("invalid buffer size") + } + ct.Coefficients = make([]fr.Element, ctLen) + for i := uint64(0); i < ctLen; i++ { + var c fr.Element + k := int(i) * fr.Bytes + for j := 0; j < fr.Limbs; j++ { + c[j] = binary.LittleEndian.Uint32(buf[k+j*4 : k+(j+1)*4]) + } + ct.Coefficients[i] = c + } + return nil +} + +func (ct *CoeffTable) AddCoeff(coeff constraint.U32) uint32 { + c := (*fr.Element)(coeff[:]) + var cID uint32 + if c.IsZero() { + cID = constraint.CoeffIdZero + } else if c.IsOne() { + cID = constraint.CoeffIdOne + } else if c.Equal(&two) { + cID = constraint.CoeffIdTwo + } else if c.Equal(&minusOne) { + cID = constraint.CoeffIdMinusOne + } else if c.Equal(&minusTwo) { + cID = constraint.CoeffIdMinusTwo + } else { + cc := *c + if id, ok := ct.mCoeffs[cc]; ok { + cID = id + } else { + cID = uint32(len(ct.Coefficients)) + ct.Coefficients = append(ct.Coefficients, cc) + ct.mCoeffs[cc] = cID + } + } + return cID +} + +func (ct *CoeffTable) MakeTerm(coeff constraint.U32, variableID int) constraint.Term { + cID := ct.AddCoeff(coeff) + return constraint.Term{VID: uint32(variableID), CID: cID} +} + +// CoeffToString implements constraint.Resolver +func (ct *CoeffTable) CoeffToString(cID int) string { + return ct.Coefficients[cID].String() +} + +// implements constraint.Field +type field struct{} + +var _ constraint.Field[constraint.U32] = &field{} + +var ( + two fr.Element + minusOne fr.Element + minusTwo fr.Element +) + +func init() { + minusOne.SetOne() + minusOne.Neg(&minusOne) + two.SetOne() + two.Double(&two) + minusTwo.Neg(&two) +} + +func (engine *field) FromInterface(i interface{}) constraint.U32 { + var e fr.Element + if _, err := e.SetInterface(i); err != nil { + // need to clean that --> some code path are dissimilar + // for example setting a fr.Element from an fp.Element + // fails with the above but succeeds through big int... (2-chains) + b := utils.FromInterface(i) + e.SetBigInt(&b) + } + var r constraint.U32 + copy(r[:], e[:]) + return r +} +func (engine *field) ToBigInt(c constraint.U32) *big.Int { + e := (*fr.Element)(c[:]) + r := new(big.Int) + e.BigInt(r) + return r + +} +func (engine *field) Mul(a, b constraint.U32) constraint.U32 { + _a := (*fr.Element)(a[:]) + _b := (*fr.Element)(b[:]) + _a.Mul(_a, _b) + return a +} + +func (engine *field) Add(a, b constraint.U32) constraint.U32 { + _a := (*fr.Element)(a[:]) + _b := (*fr.Element)(b[:]) + _a.Add(_a, _b) + return a +} +func (engine *field) Sub(a, b constraint.U32) constraint.U32 { + _a := (*fr.Element)(a[:]) + _b := (*fr.Element)(b[:]) + _a.Sub(_a, _b) + return a +} +func (engine *field) Neg(a constraint.U32) constraint.U32 { + e := (*fr.Element)(a[:]) + e.Neg(e) + return a + +} +func (engine *field) Inverse(a constraint.U32) (constraint.U32, bool) { + if a.IsZero() { + return a, false + } + e := (*fr.Element)(a[:]) + if e.IsZero() { + return a, false + } else if e.IsOne() { + return a, true + } + var t fr.Element + t.Neg(e) + if t.IsOne() { + return a, true + } + + e.Inverse(e) + return a, true +} + +func (engine *field) IsOne(a constraint.U32) bool { + e := (*fr.Element)(a[:]) + return e.IsOne() +} + +func (engine *field) One() constraint.U32 { + e := fr.One() + var r constraint.U32 + copy(r[:], e[:]) + return r +} + +func (engine *field) String(a constraint.U32) string { + e := (*fr.Element)(a[:]) + return e.String() +} + +func (engine *field) Uint64(a constraint.U32) (uint64, bool) { + e := (*fr.Element)(a[:]) + if !e.IsUint64() { + return 0, false + } + return e.Uint64(), true +} diff --git a/constraint/babybear/marshal.go b/constraint/babybear/marshal.go new file mode 100644 index 00000000..e85ecc5d --- /dev/null +++ b/constraint/babybear/marshal.go @@ -0,0 +1,90 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package cs + +import ( + "encoding/binary" + "fmt" + "io" + + "github.com/blang/semver/v4" +) + +// WriteTo encodes R1CS into provided io.Writer using cbor +func (cs *system) WriteTo(w io.Writer) (int64, error) { + b, err := cs.System.ToBytes() + if err != nil { + return 0, err + } + + c := cs.CoeffTable.toBytes() + + totalLen := uint64(len(b) + len(c)) + gnarkVersion := semver.MustParse(cs.GnarkVersion) + // write totalLen, gnarkVersion.Major, gnarkVersion.Minor, gnarkVersion.Patch using + // binary.LittleEndian + if err := binary.Write(w, binary.LittleEndian, totalLen); err != nil { + return 0, err + } + if err := binary.Write(w, binary.LittleEndian, gnarkVersion.Major); err != nil { + return 0, err + } + if err := binary.Write(w, binary.LittleEndian, gnarkVersion.Minor); err != nil { + return 0, err + } + if err := binary.Write(w, binary.LittleEndian, gnarkVersion.Patch); err != nil { + return 0, err + } + + // write the system + n, err := w.Write(b) + if err != nil { + return int64(n), err + } + + // write the coeff table + m, err := w.Write(c) + return int64(n+m) + 4*8, err +} + +// ReadFrom attempts to decode R1CS from io.Reader using cbor +func (cs *system) ReadFrom(r io.Reader) (int64, error) { + var totalLen uint64 + if err := binary.Read(r, binary.LittleEndian, &totalLen); err != nil { + return 0, err + } + + var major, minor, patch uint64 + if err := binary.Read(r, binary.LittleEndian, &major); err != nil { + return 0, err + } + if err := binary.Read(r, binary.LittleEndian, &minor); err != nil { + return 0, err + } + if err := binary.Read(r, binary.LittleEndian, &patch); err != nil { + return 0, err + } + // TODO @gbotrel validate version, duplicate logic with core.go CheckSerializationHeader + if major != 0 || minor < 10 { + return 0, fmt.Errorf("unsupported gnark version %d.%d.%d", major, minor, patch) + } + + data := make([]byte, totalLen) + if _, err := io.ReadFull(r, data); err != nil { + return 0, err + } + n, err := cs.System.FromBytes(data) + if err != nil { + return 0, err + } + data = data[n:] + + if err := cs.CoeffTable.fromBytes(data); err != nil { + return 0, err + } + + return int64(totalLen) + 4*8, nil +} diff --git a/constraint/babybear/r1cs_test.go b/constraint/babybear/r1cs_test.go new file mode 100644 index 00000000..ea08cd0d --- /dev/null +++ b/constraint/babybear/r1cs_test.go @@ -0,0 +1,183 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package cs_test + +import ( + "bytes" + "reflect" + "testing" + + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/frontend/cs/scs" + "github.com/consensys/gnark/internal/backend/circuits" + "github.com/consensys/gnark/internal/widecommitter" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + cs "github.com/consensys/gnark/constraint/babybear" + + fr "github.com/consensys/gnark-crypto/field/babybear" +) + +func TestSerialization(t *testing.T) { + + var buffer, buffer2 bytes.Buffer + + for name := range circuits.Circuits { + t.Run(name, func(t *testing.T) { + tc := circuits.Circuits[name] + builder := r1cs.NewBuilder[constraint.U32] + if name == "commit" { + // smallfield builders do not support commitment. We use the wrapper which has the methods + builder = widecommitter.From(builder) + } + + r1cs1, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) + if err != nil { + t.Fatal(err) + } + if testing.Short() && r1cs1.GetNbConstraints() > 50 { + return + } + + // compile a second time to ensure determinism + r1cs2, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) + if err != nil { + t.Fatal(err) + } + + { + buffer.Reset() + t.Log(name) + var err error + var written, read int64 + written, err = r1cs1.WriteTo(&buffer) + if err != nil { + t.Fatal(err) + } + var reconstructed cs.R1CS + read, err = reconstructed.ReadFrom(&buffer) + if err != nil { + t.Fatal(err) + } + if written != read { + t.Fatal("didn't read same number of bytes we wrote") + } + + // compare original and reconstructed + if diff := cmp.Diff(r1cs1, &reconstructed, + cmpopts.IgnoreFields(cs.R1CS{}, + "System.q", + "field", + "CoeffTable.mCoeffs", + "System.lbWireLevel", + "System.genericHint", + "System.SymbolTable", + "System.bitLen")); diff != "" { + t.Fatalf("round trip mismatch (-want +got):\n%s", diff) + } + } + + // ensure determinism in compilation / serialization / reconstruction + { + buffer.Reset() + n, err := r1cs1.WriteTo(&buffer) + if err != nil { + t.Fatal(err) + } + if n == 0 { + t.Fatal("No bytes are written") + } + + buffer2.Reset() + _, err = r1cs2.WriteTo(&buffer2) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(buffer.Bytes(), buffer2.Bytes()) { + t.Fatal("compilation of R1CS is not deterministic") + } + + var r, r2 cs.R1CS + n, err = r.ReadFrom(&buffer) + if err != nil { + t.Fatal(nil) + } + if n == 0 { + t.Fatal("No bytes are read") + } + _, err = r2.ReadFrom(&buffer2) + if err != nil { + t.Fatal(nil) + } + + if !reflect.DeepEqual(r, r2) { + t.Fatal("compilation of R1CS is not deterministic (reconstruction)") + } + } + }) + + } +} + +const n = 10000 + +type circuit struct { + X frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (circuit *circuit) Define(api frontend.API) error { + for i := 0; i < n; i++ { + circuit.X = api.Add(api.Mul(circuit.X, circuit.X), circuit.X, 42) + } + api.AssertIsEqual(circuit.X, circuit.Y) + return nil +} + +func BenchmarkSolve(b *testing.B) { + + var w circuit + w.X = 1 + w.Y = 1 + witness, err := frontend.NewWitness(&w, fr.Modulus()) + if err != nil { + b.Fatal(err) + } + + b.Run("scs", func(b *testing.B) { + var c circuit + ccs, err := frontend.CompileGeneric[constraint.U32](fr.Modulus(), scs.NewBuilder, &c) + if err != nil { + b.Fatal(err) + } + b.Log("scs nbConstraints", ccs.GetNbConstraints()) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = ccs.IsSolved(witness) + } + }) + + b.Run("r1cs", func(b *testing.B) { + var c circuit + ccs, err := frontend.CompileGeneric[constraint.U32](fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) + if err != nil { + b.Fatal(err) + } + b.Log("r1cs nbConstraints", ccs.GetNbConstraints()) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = ccs.IsSolved(witness) + } + }) + +} diff --git a/constraint/babybear/solver.go b/constraint/babybear/solver.go new file mode 100644 index 00000000..410fc036 --- /dev/null +++ b/constraint/babybear/solver.go @@ -0,0 +1,638 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package cs + +import ( + "errors" + "fmt" + "math" + "math/big" + "strconv" + "strings" + "sync" + "sync/atomic" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/pool" + "github.com/consensys/gnark/constraint" + csolver "github.com/consensys/gnark/constraint/solver" + "github.com/rs/zerolog" + + fr "github.com/consensys/gnark-crypto/field/babybear" +) + +// solver represent the state of the solver during a call to System.Solve(...) +type solver struct { + *system + + // values and solved are index by the wire (variable) id + values []fr.Element + solved []bool + nbSolved uint64 + + // maps hintID to hint function + mHintsFunctions map[csolver.HintID]csolver.Hint + + // used to out api.Println + logger zerolog.Logger + nbTasks int + + a, b, c fr.Vector // R1CS solver will compute the a,b,c matrices + + q *big.Int +} + +func newSolver(cs *system, witness fr.Vector, opts ...csolver.Option) (*solver, error) { + // parse options + opt, err := csolver.NewConfig(opts...) + if err != nil { + return nil, err + } + + // check witness size + witnessOffset := 0 + if cs.Type == constraint.SystemR1CS { + witnessOffset++ + } + + nbWires := len(cs.Public) + len(cs.Secret) + cs.NbInternalVariables + expectedWitnessSize := len(cs.Public) - witnessOffset + len(cs.Secret) + + if len(witness) != expectedWitnessSize { + return nil, fmt.Errorf("invalid witness size, got %d, expected %d", len(witness), expectedWitnessSize) + } + + // check all hints are there + hintFunctions := opt.HintFunctions + + // hintsDependencies is from compile time; it contains the list of hints the solver **needs** + var missing []string + for hintUUID, hintID := range cs.MHintsDependencies { + if _, ok := hintFunctions[hintUUID]; !ok { + missing = append(missing, hintID) + } + } + + if len(missing) > 0 { + return nil, fmt.Errorf("solver missing hint(s): %v", missing) + } + + s := solver{ + system: cs, + values: make([]fr.Element, nbWires), + solved: make([]bool, nbWires), + mHintsFunctions: hintFunctions, + logger: opt.Logger, + nbTasks: opt.NbTasks, + q: cs.Field(), + } + + // set the witness indexes as solved + if witnessOffset == 1 { + s.solved[0] = true // ONE_WIRE + s.values[0].SetOne() + } + copy(s.values[witnessOffset:], witness) + for i := range witness { + s.solved[i+witnessOffset] = true + } + + // keep track of the number of wire instantiations we do, for a post solve sanity check + // to ensure we instantiated all wires + s.nbSolved += uint64(len(witness) + witnessOffset) + + if s.Type == constraint.SystemR1CS { + n := ecc.NextPowerOfTwo(uint64(cs.GetNbConstraints())) + s.a = make(fr.Vector, cs.GetNbConstraints(), n) + s.b = make(fr.Vector, cs.GetNbConstraints(), n) + s.c = make(fr.Vector, cs.GetNbConstraints(), n) + } + + return &s, nil +} + +func (s *solver) set(id int, value fr.Element) { + if s.solved[id] { + panic("solving the same wire twice should never happen.") + } + s.values[id] = value + s.solved[id] = true + atomic.AddUint64(&s.nbSolved, 1) +} + +// computeTerm computes coeff*variable +func (s *solver) computeTerm(t constraint.Term) fr.Element { + cID, vID := t.CoeffID(), t.WireID() + + if t.IsConstant() { + return s.Coefficients[cID] + } + + if cID != 0 && !s.solved[vID] { + panic("computing a term with an unsolved wire") + } + + switch cID { + case constraint.CoeffIdZero: + return fr.Element{} + case constraint.CoeffIdOne: + return s.values[vID] + case constraint.CoeffIdTwo: + var res fr.Element + res.Double(&s.values[vID]) + return res + case constraint.CoeffIdMinusOne: + var res fr.Element + res.Neg(&s.values[vID]) + return res + default: + var res fr.Element + res.Mul(&s.Coefficients[cID], &s.values[vID]) + return res + } +} + +// r += (t.coeff*t.value) +// TODO @gbotrel check t.IsConstant on the caller side when necessary +func (s *solver) accumulateInto(t constraint.Term, r *fr.Element) { + cID := t.CoeffID() + vID := t.WireID() + + if t.IsConstant() { + r.Add(r, &s.Coefficients[cID]) + return + } + + switch cID { + case constraint.CoeffIdZero: + return + case constraint.CoeffIdOne: + r.Add(r, &s.values[vID]) + case constraint.CoeffIdTwo: + var res fr.Element + res.Double(&s.values[vID]) + r.Add(r, &res) + case constraint.CoeffIdMinusOne: + r.Sub(r, &s.values[vID]) + default: + var res fr.Element + res.Mul(&s.Coefficients[cID], &s.values[vID]) + r.Add(r, &res) + } +} + +// solveWithHint executes a hint and assign the result to its defined outputs. +func (s *solver) solveWithHint(h *constraint.HintMapping) error { + // ensure hint function was provided + f, ok := s.mHintsFunctions[h.HintID] + if !ok { + return errors.New("missing hint function") + } + + // tmp IO big int memory + nbInputs := len(h.Inputs) + nbOutputs := int(h.OutputRange.End - h.OutputRange.Start) + inputs := make([]*big.Int, nbInputs) + outputs := make([]*big.Int, nbOutputs) + for i := 0; i < nbOutputs; i++ { + outputs[i] = pool.BigInt.Get() + outputs[i].SetUint64(0) + } + + q := pool.BigInt.Get() + q.Set(s.q) + + for i := 0; i < nbInputs; i++ { + var v fr.Element + for _, term := range h.Inputs[i] { + if term.IsConstant() { + v.Add(&v, &s.Coefficients[term.CoeffID()]) + continue + } + s.accumulateInto(term, &v) + } + inputs[i] = pool.BigInt.Get() + v.BigInt(inputs[i]) + } + + err := f(q, inputs, outputs) + + var v fr.Element + for i := range outputs { + v.SetBigInt(outputs[i]) + s.set(int(h.OutputRange.Start)+i, v) + pool.BigInt.Put(outputs[i]) + } + + for i := range inputs { + pool.BigInt.Put(inputs[i]) + } + + pool.BigInt.Put(q) + + return err +} + +func (s *solver) printLogs(logs []constraint.LogEntry) { + if s.logger.GetLevel() == zerolog.Disabled { + return + } + + for i := 0; i < len(logs); i++ { + logLine := s.logValue(logs[i]) + s.logger.Debug().Str(zerolog.CallerFieldName, logs[i].Caller).Msg(logLine) + } +} + +const unsolvedVariable = "" + +func (s *solver) logValue(log constraint.LogEntry) string { + var toResolve []interface{} + var ( + eval fr.Element + missingValue bool + ) + for j := 0; j < len(log.ToResolve); j++ { + // before eval le + + missingValue = false + eval.SetZero() + + for _, t := range log.ToResolve[j] { + // for each term in the linear expression + + cID, vID := t.CoeffID(), t.WireID() + if t.IsConstant() { + // just add the constant + eval.Add(&eval, &s.Coefficients[cID]) + continue + } + + if !s.solved[vID] { + missingValue = true + break // stop the loop we can't evaluate. + } + + tv := s.computeTerm(t) + eval.Add(&eval, &tv) + } + + // after + if missingValue { + toResolve = append(toResolve, unsolvedVariable) + } else { + // we have to append our accumulator + toResolve = append(toResolve, eval.String()) + } + + } + if len(log.Stack) > 0 { + var sbb strings.Builder + for _, lID := range log.Stack { + location := s.SymbolTable.Locations[lID] + function := s.SymbolTable.Functions[location.FunctionID] + + sbb.WriteString(function.Name) + sbb.WriteByte('\n') + sbb.WriteByte('\t') + sbb.WriteString(function.Filename) + sbb.WriteByte(':') + sbb.WriteString(strconv.Itoa(int(location.Line))) + sbb.WriteByte('\n') + } + toResolve = append(toResolve, sbb.String()) + } + return fmt.Sprintf(log.Format, toResolve...) +} + +// divByCoeff sets res = res / t.Coeff +func (solver *solver) divByCoeff(res *fr.Element, cID uint32) { + switch cID { + case constraint.CoeffIdOne: + return + case constraint.CoeffIdMinusOne: + res.Neg(res) + case constraint.CoeffIdZero: + panic("division by 0") + default: + // this is slow, but shouldn't happen as divByCoeff is called to + // remove the coeff of an unsolved wire + // but unsolved wires are (in gnark frontend) systematically set with a coeff == 1 or -1 + res.Div(res, &solver.Coefficients[cID]) + } +} + +// Implement constraint.Solver +func (s *solver) GetValue(cID, vID uint32) constraint.U32 { + var r constraint.U32 + e := s.computeTerm(constraint.Term{CID: cID, VID: vID}) + copy(r[:], e[:]) + return r +} +func (s *solver) GetCoeff(cID uint32) constraint.U32 { + var r constraint.U32 + copy(r[:], s.Coefficients[cID][:]) + return r +} +func (s *solver) SetValue(vID uint32, f constraint.U32) { + s.set(int(vID), *(*fr.Element)(f[:])) +} + +func (s *solver) IsSolved(vID uint32) bool { + return s.solved[vID] +} + +// Read interprets input calldata as either a LinearExpression (if R1CS) or a Term (if Plonkish), +// evaluates it and return the result and the number of uint32 word read. +func (s *solver) Read(calldata []uint32) (constraint.U32, int) { + if s.Type == constraint.SystemSparseR1CS { + if calldata[0] != 1 { + panic("invalid calldata") + } + return s.GetValue(calldata[1], calldata[2]), 3 + } + var r fr.Element + n := int(calldata[0]) + j := 1 + for k := 0; k < n; k++ { + // we read k Terms + s.accumulateInto(constraint.Term{CID: calldata[j], VID: calldata[j+1]}, &r) + j += 2 + } + + var ret constraint.U32 + copy(ret[:], r[:]) + return ret, j +} + +// processInstruction decodes the instruction and execute blueprint-defined logic. +// an instruction can encode a hint, a custom constraint or a generic constraint. +func (solver *solver) processInstruction(pi constraint.PackedInstruction, scratch *scratch) error { + // fetch the blueprint + blueprint := solver.Blueprints[pi.BlueprintID] + inst := pi.Unpack(&solver.System) + cID := inst.ConstraintOffset // here we have 1 constraint in the instruction only + + if solver.Type == constraint.SystemR1CS { + if bc, ok := blueprint.(constraint.BlueprintR1C); ok { + // TODO @gbotrel we use the solveR1C method for now, having user-defined + // blueprint for R1CS would require constraint.Solver interface to add methods + // to set a,b,c since it's more efficient to compute these while we solve. + bc.DecompressR1C(&scratch.tR1C, inst) + return solver.solveR1C(cID, &scratch.tR1C) + } + } + + // blueprint declared "I know how to solve this." + if bc, ok := blueprint.(constraint.BlueprintSolvable[constraint.U32]); ok { + if err := bc.Solve(solver, inst); err != nil { + return solver.wrapErrWithDebugInfo(cID, err) + } + return nil + } + + // blueprint encodes a hint, we execute. + // TODO @gbotrel may be worth it to move hint logic in blueprint "solve" + if bc, ok := blueprint.(constraint.BlueprintHint); ok { + bc.DecompressHint(&scratch.tHint, inst) + return solver.solveWithHint(&scratch.tHint) + } + + return nil +} + +// run runs the solver. it return an error if a constraint is not satisfied or if not all wires +// were instantiated. +func (solver *solver) run() error { + // minWorkPerCPU is the minimum target number of constraint a task should hold + // in other words, if a level has less than minWorkPerCPU, it will not be parallelized and executed + // sequentially without sync. + const minWorkPerCPU = 50.0 // TODO @gbotrel revisit that with blocks. + + // cs.Levels has a list of levels, where all constraints in a level l(n) are independent + // and may only have dependencies on previous levels + // for each constraint + // we are guaranteed that each R1C contains at most one unsolved wire + // first we solve the unsolved wire (if any) + // then we check that the constraint is valid + // if a[i] * b[i] != c[i]; it means the constraint is not satisfied + var wg sync.WaitGroup + chTasks := make(chan []uint32, solver.nbTasks) + chError := make(chan error, solver.nbTasks) + + // start a worker pool + // each worker wait on chTasks + // a task is a slice of constraint indexes to be solved + for i := 0; i < solver.nbTasks; i++ { + go func() { + var scratch scratch + for t := range chTasks { + for _, i := range t { + if err := solver.processInstruction(solver.Instructions[i], &scratch); err != nil { + chError <- err + wg.Done() + return + } + } + wg.Done() + } + }() + } + + // clean up pool go routines + defer func() { + close(chTasks) + close(chError) + }() + + var scratch scratch + + // for each level, we push the tasks + for _, level := range solver.Levels { + + // max CPU to use + maxCPU := float64(len(level)) / minWorkPerCPU + + if maxCPU <= 1.0 || solver.nbTasks == 1 { + // we do it sequentially + for _, i := range level { + if err := solver.processInstruction(solver.Instructions[i], &scratch); err != nil { + return err + } + } + continue + } + + // number of tasks for this level is set to number of CPU + // but if we don't have enough work for all our CPU, it can be lower. + nbTasks := solver.nbTasks + maxTasks := int(math.Ceil(maxCPU)) + if nbTasks > maxTasks { + nbTasks = maxTasks + } + nbIterationsPerCpus := len(level) / nbTasks + + // more CPUs than tasks: a CPU will work on exactly one iteration + // note: this depends on minWorkPerCPU constant + if nbIterationsPerCpus < 1 { + nbIterationsPerCpus = 1 + nbTasks = len(level) + } + + extraTasks := len(level) - (nbTasks * nbIterationsPerCpus) + extraTasksOffset := 0 + + for i := 0; i < nbTasks; i++ { + wg.Add(1) + _start := i*nbIterationsPerCpus + extraTasksOffset + _end := _start + nbIterationsPerCpus + if extraTasks > 0 { + _end++ + extraTasks-- + extraTasksOffset++ + } + // since we're never pushing more than num CPU tasks + // we will never be blocked here + chTasks <- level[_start:_end] + } + + // wait for the level to be done + wg.Wait() + + if len(chError) > 0 { + return <-chError + } + } + + if int(solver.nbSolved) != len(solver.values) { + return errors.New("solver didn't assign a value to all wires") + } + + return nil +} + +// solveR1C compute unsolved wires in the constraint, if any and set the solver accordingly +// +// returns an error if the solver called a hint function that errored +// returns false, nil if there was no wire to solve +// returns true, nil if exactly one wire was solved. In that case, it is redundant to check that +// the constraint is satisfied later. +func (solver *solver) solveR1C(cID uint32, r *constraint.R1C) error { + a, b, c := &solver.a[cID], &solver.b[cID], &solver.c[cID] + + // the index of the non-zero entry shows if L, R or O has an uninstantiated wire + // the content is the ID of the wire non instantiated + var loc uint8 + + var termToCompute constraint.Term + + processLExp := func(l constraint.LinearExpression, val *fr.Element, locValue uint8) { + for _, t := range l { + vID := t.WireID() + + // wire is already computed, we just accumulate in val + if solver.solved[vID] { + solver.accumulateInto(t, val) + continue + } + + if loc != 0 { + panic("found more than one wire to instantiate") + } + termToCompute = t + loc = locValue + } + } + + processLExp(r.L, a, 1) + processLExp(r.R, b, 2) + processLExp(r.O, c, 3) + + if loc == 0 { + // there is nothing to solve, may happen if we have an assertion + // (ie a constraints that doesn't yield any output) + // or if we solved the unsolved wires with hint functions + var check fr.Element + if !check.Mul(a, b).Equal(c) { + return solver.wrapErrWithDebugInfo(cID, fmt.Errorf("%s ⋅ %s != %s", a.String(), b.String(), c.String())) + } + return nil + } + + // we compute the wire value and instantiate it + wID := termToCompute.WireID() + + // solver result + var wire fr.Element + + switch loc { + case 1: + if !b.IsZero() { + wire.Div(c, b). + Sub(&wire, a) + a.Add(a, &wire) + } else { + // we didn't actually ensure that a * b == c + var check fr.Element + if !check.Mul(a, b).Equal(c) { + return solver.wrapErrWithDebugInfo(cID, fmt.Errorf("%s ⋅ %s != %s", a.String(), b.String(), c.String())) + } + } + case 2: + if !a.IsZero() { + wire.Div(c, a). + Sub(&wire, b) + b.Add(b, &wire) + } else { + var check fr.Element + if !check.Mul(a, b).Equal(c) { + return solver.wrapErrWithDebugInfo(cID, fmt.Errorf("%s ⋅ %s != %s", a.String(), b.String(), c.String())) + } + } + case 3: + wire.Mul(a, b). + Sub(&wire, c) + + c.Add(c, &wire) + } + + // wire is the term (coeff * value) + // but in the solver we want to store the value only + // note that in gnark frontend, coeff here is always 1 or -1 + solver.divByCoeff(&wire, termToCompute.CID) + solver.set(wID, wire) + + return nil +} + +// UnsatisfiedConstraintError wraps an error with useful metadata on the unsatisfied constraint +type UnsatisfiedConstraintError struct { + Err error + CID int // constraint ID + DebugInfo *string // optional debug info +} + +func (r *UnsatisfiedConstraintError) Error() string { + if r.DebugInfo != nil { + return fmt.Sprintf("constraint #%d is not satisfied: %s", r.CID, *r.DebugInfo) + } + return fmt.Sprintf("constraint #%d is not satisfied: %s", r.CID, r.Err.Error()) +} + +func (solver *solver) wrapErrWithDebugInfo(cID uint32, err error) *UnsatisfiedConstraintError { + var debugInfo *string + if dID, ok := solver.MDebug[int(cID)]; ok { + debugInfo = new(string) + *debugInfo = solver.logValue(solver.DebugInfo[dID]) + } + return &UnsatisfiedConstraintError{CID: int(cID), Err: err, DebugInfo: debugInfo} +} + +// temporary variables to avoid memallocs in hotloop +type scratch struct { + tR1C constraint.R1C + tHint constraint.HintMapping +} diff --git a/constraint/babybear/system.go b/constraint/babybear/system.go new file mode 100644 index 00000000..4da35e30 --- /dev/null +++ b/constraint/babybear/system.go @@ -0,0 +1,294 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package cs + +import ( + "io" + "time" + + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" + csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/internal/gkr/gkrinfo" + "github.com/consensys/gnark/logger" + + "github.com/consensys/gnark-crypto/ecc" + + fr "github.com/consensys/gnark-crypto/field/babybear" +) + +type R1CS = system +type SparseR1CS = system + +// system is a curved-typed constraint.System with a concrete coefficient table (fr.Element) +type system struct { + constraint.System + CoeffTable + field +} + +// NewR1CS is a constructor for R1CS. It is meant to be use by gnark frontend only, +// and should not be used by gnark users. See groth16.NewCS(...) instead. +func NewR1CS(capacity int) *R1CS { + return newSystem(capacity, constraint.SystemR1CS) +} + +// NewSparseR1CS is a constructor for SparseR1CS. It is meant to be use by gnark frontend only, +// and should not be used by gnark users. See plonk.NewCS(...) instead. +func NewSparseR1CS(capacity int) *SparseR1CS { + return newSystem(capacity, constraint.SystemSparseR1CS) +} + +func newSystem(capacity int, t constraint.SystemType) *system { + return &system{ + System: constraint.NewSystem(fr.Modulus(), capacity, t), + CoeffTable: newCoeffTable(capacity / 10), + } +} + +// Solve solves the constraint system with provided witness. +// If it's a R1CS returns R1CSSolution +// If it's a SparseR1CS returns SparseR1CSSolution +func (cs *system) Solve(witness witness.Witness, opts ...csolver.Option) (any, error) { + log := logger.Logger().With().Int("nbConstraints", cs.GetNbConstraints()).Logger() + start := time.Now() + + v := witness.Vector().(fr.Vector) + + // init the solver + solver, err := newSolver(cs, v, opts...) + if err != nil { + log.Err(err).Send() + return nil, err + } + + // reset the stateful blueprints + for i := range cs.Blueprints { + if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful[constraint.U32]); ok { + b.Reset() + } + } + + // defer log printing once all solver.values are computed + // (or sooner, if a constraint is not satisfied) + defer solver.printLogs(cs.Logs) + + // run it. + if err := solver.run(); err != nil { + log.Err(err).Send() + return nil, err + } + + log.Debug().Dur("took", time.Since(start)).Msg("constraint system solver done") + + // format the solution + // TODO @gbotrel revisit post-refactor + if cs.Type == constraint.SystemR1CS { + var res R1CSSolution + res.W = solver.values + res.A = solver.a + res.B = solver.b + res.C = solver.c + return &res, nil + } else { + // sparse R1CS + var res SparseR1CSSolution + // query l, r, o in Lagrange basis, not blinded + res.L, res.R, res.O = evaluateLROSmallDomain(cs, solver.values) + + return &res, nil + } + +} + +// IsSolved +// Deprecated: use _, err := Solve(...) instead +func (cs *system) IsSolved(witness witness.Witness, opts ...csolver.Option) error { + _, err := cs.Solve(witness, opts...) + return err +} + +// GetR1Cs return the list of R1C +func (cs *system) GetR1Cs() []constraint.R1C { + toReturn := make([]constraint.R1C, 0, cs.GetNbConstraints()) + + for _, inst := range cs.Instructions { + blueprint := cs.Blueprints[inst.BlueprintID] + if bc, ok := blueprint.(constraint.BlueprintR1C); ok { + var r1c constraint.R1C + bc.DecompressR1C(&r1c, inst.Unpack(&cs.System)) + toReturn = append(toReturn, r1c) + } + } + return toReturn +} + +// GetNbCoefficients return the number of unique coefficients needed in the R1CS +func (cs *system) GetNbCoefficients() int { + return len(cs.Coefficients) +} + +// CurveID returns curve ID as defined in gnark-crypto +func (cs *system) CurveID() ecc.ID { + return ecc.UNKNOWN +} + +func (cs *system) GetCoefficient(i int) (r constraint.U32) { + copy(r[:], cs.Coefficients[i][:]) + return +} + +// GetSparseR1Cs return the list of SparseR1C +func (cs *system) GetSparseR1Cs() []constraint.SparseR1C { + + toReturn := make([]constraint.SparseR1C, 0, cs.GetNbConstraints()) + + for _, inst := range cs.Instructions { + blueprint := cs.Blueprints[inst.BlueprintID] + if bc, ok := blueprint.(constraint.BlueprintSparseR1C); ok { + var sparseR1C constraint.SparseR1C + bc.DecompressSparseR1C(&sparseR1C, inst.Unpack(&cs.System)) + toReturn = append(toReturn, sparseR1C) + } + } + return toReturn +} + +// evaluateLROSmallDomain extracts the solver l, r, o, and returns it in lagrange form. +// solver = [ public | secret | internal ] +// TODO @gbotrel refactor; this seems to be a small util function for plonk +func evaluateLROSmallDomain(cs *system, solution []fr.Element) ([]fr.Element, []fr.Element, []fr.Element) { + + //s := int(pk.Domain[0].Cardinality) + s := cs.GetNbConstraints() + len(cs.Public) // len(spr.Public) is for the placeholder constraints + s = int(ecc.NextPowerOfTwo(uint64(s))) + + var l, r, o []fr.Element + l = make([]fr.Element, s, s+4) // +4 to leave room for the blinding in plonk + r = make([]fr.Element, s, s+4) + o = make([]fr.Element, s, s+4) + s0 := solution[0] + + for i := 0; i < len(cs.Public); i++ { // placeholders + l[i] = solution[i] + r[i] = s0 + o[i] = s0 + } + offset := len(cs.Public) + nbConstraints := cs.GetNbConstraints() + + var sparseR1C constraint.SparseR1C + j := 0 + for _, inst := range cs.Instructions { + blueprint := cs.Blueprints[inst.BlueprintID] + if bc, ok := blueprint.(constraint.BlueprintSparseR1C); ok { + bc.DecompressSparseR1C(&sparseR1C, inst.Unpack(&cs.System)) + + l[offset+j] = solution[sparseR1C.XA] + r[offset+j] = solution[sparseR1C.XB] + o[offset+j] = solution[sparseR1C.XC] + j++ + } + } + + offset += nbConstraints + + for i := 0; i < s-offset; i++ { // offset to reach 2**n constraints (where the id of l,r,o is 0, so we assign solver[0]) + l[offset+i] = s0 + r[offset+i] = s0 + o[offset+i] = s0 + } + + return l, r, o + +} + +// R1CSSolution represent a valid assignment to all the variables in the constraint system. +// The vector W such that Aw o Bw - Cw = 0 +type R1CSSolution struct { + W fr.Vector + A, B, C fr.Vector +} + +func (t *R1CSSolution) WriteTo(w io.Writer) (int64, error) { + n, err := t.W.WriteTo(w) + if err != nil { + return n, err + } + a, err := t.A.WriteTo(w) + n += a + if err != nil { + return n, err + } + a, err = t.B.WriteTo(w) + n += a + if err != nil { + return n, err + } + a, err = t.C.WriteTo(w) + n += a + return n, err +} + +func (t *R1CSSolution) ReadFrom(r io.Reader) (int64, error) { + n, err := t.W.ReadFrom(r) + if err != nil { + return n, err + } + a, err := t.A.ReadFrom(r) + n += a + if err != nil { + return n, err + } + a, err = t.B.ReadFrom(r) + n += a + if err != nil { + return n, err + } + a, err = t.C.ReadFrom(r) + n += a + return n, err +} + +// SparseR1CSSolution represent a valid assignment to all the variables in the constraint system. +type SparseR1CSSolution struct { + L, R, O fr.Vector +} + +func (t *SparseR1CSSolution) WriteTo(w io.Writer) (int64, error) { + n, err := t.L.WriteTo(w) + if err != nil { + return n, err + } + a, err := t.R.WriteTo(w) + n += a + if err != nil { + return n, err + } + a, err = t.O.WriteTo(w) + n += a + return n, err + +} + +func (t *SparseR1CSSolution) ReadFrom(r io.Reader) (int64, error) { + n, err := t.L.ReadFrom(r) + if err != nil { + return n, err + } + a, err := t.R.ReadFrom(r) + n += a + if err != nil { + return n, err + } + a, err = t.O.ReadFrom(r) + n += a + return n, err +} + +func (s *system) AddGkr(gkr gkrinfo.StoringInfo) error { + return s.System.AddGkr(gkr) +} diff --git a/constraint/bls12-377/coeff.go b/constraint/bls12-377/coeff.go index c9c8a2e2..bd303916 100644 --- a/constraint/bls12-377/coeff.go +++ b/constraint/bls12-377/coeff.go @@ -8,9 +8,10 @@ package cs import ( "encoding/binary" "errors" + "math/big" + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/utils" - "math/big" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" ) @@ -73,7 +74,7 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { return nil } -func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { +func (ct *CoeffTable) AddCoeff(coeff constraint.U64) uint32 { c := (*fr.Element)(coeff[:]) var cID uint32 if c.IsZero() { @@ -99,7 +100,7 @@ func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { return cID } -func (ct *CoeffTable) MakeTerm(coeff constraint.Element, variableID int) constraint.Term { +func (ct *CoeffTable) MakeTerm(coeff constraint.U64, variableID int) constraint.Term { cID := ct.AddCoeff(coeff) return constraint.Term{VID: uint32(variableID), CID: cID} } @@ -112,7 +113,7 @@ func (ct *CoeffTable) CoeffToString(cID int) string { // implements constraint.Field type field struct{} -var _ constraint.Field = &field{} +var _ constraint.Field[constraint.U64] = &field{} var ( two fr.Element @@ -128,7 +129,7 @@ func init() { minusTwo.Neg(&two) } -func (engine *field) FromInterface(i interface{}) constraint.Element { +func (engine *field) FromInterface(i interface{}) constraint.U64 { var e fr.Element if _, err := e.SetInterface(i); err != nil { // need to clean that --> some code path are dissimilar @@ -137,43 +138,43 @@ func (engine *field) FromInterface(i interface{}) constraint.Element { b := utils.FromInterface(i) e.SetBigInt(&b) } - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) ToBigInt(c constraint.Element) *big.Int { +func (engine *field) ToBigInt(c constraint.U64) *big.Int { e := (*fr.Element)(c[:]) r := new(big.Int) e.BigInt(r) return r } -func (engine *field) Mul(a, b constraint.Element) constraint.Element { +func (engine *field) Mul(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Mul(_a, _b) return a } -func (engine *field) Add(a, b constraint.Element) constraint.Element { +func (engine *field) Add(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Add(_a, _b) return a } -func (engine *field) Sub(a, b constraint.Element) constraint.Element { +func (engine *field) Sub(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Sub(_a, _b) return a } -func (engine *field) Neg(a constraint.Element) constraint.Element { +func (engine *field) Neg(a constraint.U64) constraint.U64 { e := (*fr.Element)(a[:]) e.Neg(e) return a } -func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { +func (engine *field) Inverse(a constraint.U64) (constraint.U64, bool) { if a.IsZero() { return a, false } @@ -193,24 +194,24 @@ func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { return a, true } -func (engine *field) IsOne(a constraint.Element) bool { +func (engine *field) IsOne(a constraint.U64) bool { e := (*fr.Element)(a[:]) return e.IsOne() } -func (engine *field) One() constraint.Element { +func (engine *field) One() constraint.U64 { e := fr.One() - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) String(a constraint.Element) string { +func (engine *field) String(a constraint.U64) string { e := (*fr.Element)(a[:]) return e.String() } -func (engine *field) Uint64(a constraint.Element) (uint64, bool) { +func (engine *field) Uint64(a constraint.U64) (uint64, bool) { e := (*fr.Element)(a[:]) if !e.IsUint64() { return 0, false diff --git a/constraint/bls12-377/gkr.go b/constraint/bls12-377/gkr.go deleted file mode 100644 index 948ed151..00000000 --- a/constraint/bls12-377/gkr.go +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2020-2025 Consensys Software Inc. -// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. - -// Code generated by gnark DO NOT EDIT - -package cs - -import ( - "fmt" - "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" - "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/gkr" - "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" - fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" - "github.com/consensys/gnark-crypto/utils" - "github.com/consensys/gnark/constraint" - hint "github.com/consensys/gnark/constraint/solver" - algo_utils "github.com/consensys/gnark/internal/utils" - "hash" - "math/big" - "sync" -) - -type GkrSolvingData struct { - assignments gkr.WireAssignment - circuit gkr.Circuit - memoryPool polynomial.Pool - workers *utils.WorkerPool -} - -func convertCircuit(noPtr constraint.GkrCircuit) (gkr.Circuit, error) { - resCircuit := make(gkr.Circuit, len(noPtr)) - var found bool - for i := range noPtr { - if resCircuit[i].Gate, found = gkr.Gates[noPtr[i].Gate]; !found && noPtr[i].Gate != "" { - return nil, fmt.Errorf("gate \"%s\" not found", noPtr[i].Gate) - } - resCircuit[i].Inputs = algo_utils.Map(noPtr[i].Inputs, algo_utils.SlicePtrAt(resCircuit)) - } - return resCircuit, nil -} - -func (d *GkrSolvingData) init(info constraint.GkrInfo) (assignment gkrAssignment, err error) { - if d.circuit, err = convertCircuit(info.Circuit); err != nil { - return - } - d.memoryPool = polynomial.NewPool(d.circuit.MemoryRequirements(info.NbInstances)...) - d.workers = utils.NewWorkerPool() - - assignment = make(gkrAssignment, len(d.circuit)) - d.assignments = make(gkr.WireAssignment, len(d.circuit)) - for i := range assignment { - assignment[i] = d.memoryPool.Make(info.NbInstances) - d.assignments[&d.circuit[i]] = assignment[i] - } - return -} - -func (d *GkrSolvingData) dumpAssignments() { - for _, p := range d.assignments { - d.memoryPool.Dump(p) - } -} - -// this module assumes that wire and instance indexes respect dependencies - -type gkrAssignment [][]fr.Element //gkrAssignment is indexed wire first, instance second - -func (a gkrAssignment) setOuts(circuit constraint.GkrCircuit, outs []*big.Int) { - outsI := 0 - for i := range circuit { - if circuit[i].IsOutput() { - for j := range a[i] { - a[i][j].BigInt(outs[outsI]) - outsI++ - } - } - } - // Check if outsI == len(outs)? -} - -func GkrSolveHint(info constraint.GkrInfo, solvingData *GkrSolvingData) hint.Hint { - return func(_ *big.Int, ins, outs []*big.Int) error { - // assumes assignmentVector is arranged wire first, instance second in order of solution - circuit := info.Circuit - nbInstances := info.NbInstances - offsets := info.AssignmentOffsets() - assignment, err := solvingData.init(info) - if err != nil { - return err - } - chunks := circuit.Chunks(nbInstances) - - solveTask := func(chunkOffset int) utils.Task { - return func(startInChunk, endInChunk int) { - start := startInChunk + chunkOffset - end := endInChunk + chunkOffset - inputs := solvingData.memoryPool.Make(info.MaxNIns) - dependencyHeads := make([]int, len(circuit)) - for wI, w := range circuit { - dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { - return w.Dependencies[i].InputInstance - }, len(w.Dependencies), start) - } - - for instanceI := start; instanceI < end; instanceI++ { - for wireI, wire := range circuit { - if wire.IsInput() { - if dependencyHeads[wireI] < len(wire.Dependencies) && instanceI == wire.Dependencies[dependencyHeads[wireI]].InputInstance { - dep := wire.Dependencies[dependencyHeads[wireI]] - assignment[wireI][instanceI].Set(&assignment[dep.OutputWire][dep.OutputInstance]) - dependencyHeads[wireI]++ - } else { - assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) - } - } else { - // assemble the inputs - inputIndexes := info.Circuit[wireI].Inputs - for i, inputI := range inputIndexes { - inputs[i].Set(&assignment[inputI][instanceI]) - } - gate := solvingData.circuit[wireI].Gate - assignment[wireI][instanceI] = gate.Evaluate(inputs[:len(inputIndexes)]...) - } - } - } - solvingData.memoryPool.Dump(inputs) - } - } - - start := 0 - for _, end := range chunks { - solvingData.workers.Submit(end-start, solveTask(start), 1024).Wait() - start = end - } - - assignment.setOuts(info.Circuit, outs) - - return nil - } -} - -func frToBigInts(dst []*big.Int, src []fr.Element) { - for i := range src { - src[i].BigInt(dst[i]) - } -} - -func GkrProveHint(hashName string, data *GkrSolvingData) hint.Hint { - - return func(_ *big.Int, ins, outs []*big.Int) error { - insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called - b := make([]byte, fr.Bytes) - i.FillBytes(b) - return b[:] - }) - - hsh, err := GetHashBuilder(hashName) - if err != nil { - return err - } - - proof, err := gkr.Prove(data.circuit, data.assignments, fiatshamir.WithHash(hsh(), insBytes...), gkr.WithPool(&data.memoryPool), gkr.WithWorkers(data.workers)) - if err != nil { - return err - } - - // serialize proof: TODO: In gnark-crypto? - offset := 0 - for i := range proof { - for _, poly := range proof[i].PartialSumPolys { - frToBigInts(outs[offset:], poly) - offset += len(poly) - } - if proof[i].FinalEvalProof != nil { - finalEvalProof := proof[i].FinalEvalProof.([]fr.Element) - frToBigInts(outs[offset:], finalEvalProof) - offset += len(finalEvalProof) - } - } - - data.dumpAssignments() - - return nil - - } -} - -// TODO: Move to gnark-crypto -var ( - hashBuilderRegistry = make(map[string]func() hash.Hash) - hasBuilderLock sync.RWMutex -) - -func RegisterHashBuilder(name string, builder func() hash.Hash) { - hasBuilderLock.Lock() - defer hasBuilderLock.Unlock() - hashBuilderRegistry[name] = builder -} - -func GetHashBuilder(name string) (func() hash.Hash, error) { - hasBuilderLock.RLock() - defer hasBuilderLock.RUnlock() - builder, ok := hashBuilderRegistry[name] - if !ok { - return nil, fmt.Errorf("hash function not found") - } - return builder, nil -} - -// For testing purposes -type ConstPseudoHash int - -func (c ConstPseudoHash) Write(p []byte) (int, error) { - return len(p), nil -} - -func (c ConstPseudoHash) Sum([]byte) []byte { - var x fr.Element - x.SetInt64(int64(c)) - res := x.Bytes() - return res[:] -} - -func (c ConstPseudoHash) Reset() {} - -func (c ConstPseudoHash) Size() int { - return fr.Bytes -} - -func (c ConstPseudoHash) BlockSize() int { - return fr.Bytes -} diff --git a/constraint/bls12-377/r1cs_test.go b/constraint/bls12-377/r1cs_test.go index 0d4f150e..afc5da5e 100644 --- a/constraint/bls12-377/r1cs_test.go +++ b/constraint/bls12-377/r1cs_test.go @@ -7,12 +7,14 @@ package cs_test import ( "bytes" + "reflect" + "testing" + + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/internal/backend/circuits" - "reflect" - "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -29,8 +31,9 @@ func TestSerialization(t *testing.T) { for name := range circuits.Circuits { t.Run(name, func(t *testing.T) { tc := circuits.Circuits[name] + builder := r1cs.NewBuilder[constraint.U64] - r1cs1, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs1, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -39,7 +42,7 @@ func TestSerialization(t *testing.T) { } // compile a second time to ensure determinism - r1cs2, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs2, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -146,7 +149,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("scs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), scs.NewBuilder, &c) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), scs.NewBuilder, &c) if err != nil { b.Fatal(err) } @@ -160,7 +163,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("r1cs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) if err != nil { b.Fatal(err) } diff --git a/constraint/bls12-377/solver.go b/constraint/bls12-377/solver.go index f1bbcc75..f79940e3 100644 --- a/constraint/bls12-377/solver.go +++ b/constraint/bls12-377/solver.go @@ -8,11 +8,6 @@ package cs import ( "errors" "fmt" - "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/field/pool" - "github.com/consensys/gnark/constraint" - csolver "github.com/consensys/gnark/constraint/solver" - "github.com/rs/zerolog" "math" "math/big" "strconv" @@ -20,6 +15,15 @@ import ( "sync" "sync/atomic" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/pool" + "github.com/consensys/gnark/constraint" + csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/constraint/solver/gkrgates" + gkr "github.com/consensys/gnark/internal/gkr/bls12-377" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/rs/zerolog" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" ) @@ -47,10 +51,14 @@ type solver struct { func newSolver(cs *system, witness fr.Vector, opts ...csolver.Option) (*solver, error) { // add GKR options to overwrite the placeholder if cs.GkrInfo.Is() { - var gkrData GkrSolvingData + var gkrData gkr.SolvingData + solvingInfo, err := gkrtypes.StoringToSolvingInfo(cs.GkrInfo, gkrgates.Get) + if err != nil { + return nil, err + } opts = append(opts, - csolver.OverrideHint(cs.GkrInfo.SolveHintID, GkrSolveHint(cs.GkrInfo, &gkrData)), - csolver.OverrideHint(cs.GkrInfo.ProveHintID, GkrProveHint(cs.GkrInfo.HashName, &gkrData))) + csolver.OverrideHint(cs.GkrInfo.SolveHintID, gkr.SolveHint(solvingInfo, &gkrData)), + csolver.OverrideHint(cs.GkrInfo.ProveHintID, gkr.ProveHint(cs.GkrInfo.HashName, &gkrData))) } // parse options opt, err := csolver.NewConfig(opts...) @@ -332,18 +340,18 @@ func (solver *solver) divByCoeff(res *fr.Element, cID uint32) { } // Implement constraint.Solver -func (s *solver) GetValue(cID, vID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetValue(cID, vID uint32) constraint.U64 { + var r constraint.U64 e := s.computeTerm(constraint.Term{CID: cID, VID: vID}) copy(r[:], e[:]) return r } -func (s *solver) GetCoeff(cID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetCoeff(cID uint32) constraint.U64 { + var r constraint.U64 copy(r[:], s.Coefficients[cID][:]) return r } -func (s *solver) SetValue(vID uint32, f constraint.Element) { +func (s *solver) SetValue(vID uint32, f constraint.U64) { s.set(int(vID), *(*fr.Element)(f[:])) } @@ -353,7 +361,7 @@ func (s *solver) IsSolved(vID uint32) bool { // Read interprets input calldata as either a LinearExpression (if R1CS) or a Term (if Plonkish), // evaluates it and return the result and the number of uint32 word read. -func (s *solver) Read(calldata []uint32) (constraint.Element, int) { +func (s *solver) Read(calldata []uint32) (constraint.U64, int) { if s.Type == constraint.SystemSparseR1CS { if calldata[0] != 1 { panic("invalid calldata") @@ -369,7 +377,7 @@ func (s *solver) Read(calldata []uint32) (constraint.Element, int) { j += 2 } - var ret constraint.Element + var ret constraint.U64 copy(ret[:], r[:]) return ret, j } @@ -393,7 +401,7 @@ func (solver *solver) processInstruction(pi constraint.PackedInstruction, scratc } // blueprint declared "I know how to solve this." - if bc, ok := blueprint.(constraint.BlueprintSolvable); ok { + if bc, ok := blueprint.(constraint.BlueprintSolvable[constraint.U64]); ok { if err := bc.Solve(solver, inst); err != nil { return solver.wrapErrWithDebugInfo(cID, err) } diff --git a/constraint/bls12-377/system.go b/constraint/bls12-377/system.go index 5020ed4a..94e4dee6 100644 --- a/constraint/bls12-377/system.go +++ b/constraint/bls12-377/system.go @@ -12,6 +12,7 @@ import ( "github.com/consensys/gnark/backend/witness" "github.com/consensys/gnark/constraint" csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/internal/gkr/gkrinfo" "github.com/consensys/gnark/logger" "github.com/consensys/gnark-crypto/ecc" @@ -66,7 +67,7 @@ func (cs *system) Solve(witness witness.Witness, opts ...csolver.Option) (any, e // reset the stateful blueprints for i := range cs.Blueprints { - if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful); ok { + if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful[constraint.U64]); ok { b.Reset() } } @@ -135,7 +136,7 @@ func (cs *system) CurveID() ecc.ID { return ecc.BLS12_377 } -func (cs *system) GetCoefficient(i int) (r constraint.Element) { +func (cs *system) GetCoefficient(i int) (r constraint.U64) { copy(r[:], cs.Coefficients[i][:]) return } @@ -288,6 +289,6 @@ func (t *SparseR1CSSolution) ReadFrom(r io.Reader) (int64, error) { return n, err } -func (s *system) AddGkr(gkr constraint.GkrInfo) error { +func (s *system) AddGkr(gkr gkrinfo.StoringInfo) error { return s.System.AddGkr(gkr) } diff --git a/constraint/bls12-381/coeff.go b/constraint/bls12-381/coeff.go index c19579d3..58ab8350 100644 --- a/constraint/bls12-381/coeff.go +++ b/constraint/bls12-381/coeff.go @@ -8,9 +8,10 @@ package cs import ( "encoding/binary" "errors" + "math/big" + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/utils" - "math/big" "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" ) @@ -73,7 +74,7 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { return nil } -func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { +func (ct *CoeffTable) AddCoeff(coeff constraint.U64) uint32 { c := (*fr.Element)(coeff[:]) var cID uint32 if c.IsZero() { @@ -99,7 +100,7 @@ func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { return cID } -func (ct *CoeffTable) MakeTerm(coeff constraint.Element, variableID int) constraint.Term { +func (ct *CoeffTable) MakeTerm(coeff constraint.U64, variableID int) constraint.Term { cID := ct.AddCoeff(coeff) return constraint.Term{VID: uint32(variableID), CID: cID} } @@ -112,7 +113,7 @@ func (ct *CoeffTable) CoeffToString(cID int) string { // implements constraint.Field type field struct{} -var _ constraint.Field = &field{} +var _ constraint.Field[constraint.U64] = &field{} var ( two fr.Element @@ -128,7 +129,7 @@ func init() { minusTwo.Neg(&two) } -func (engine *field) FromInterface(i interface{}) constraint.Element { +func (engine *field) FromInterface(i interface{}) constraint.U64 { var e fr.Element if _, err := e.SetInterface(i); err != nil { // need to clean that --> some code path are dissimilar @@ -137,43 +138,43 @@ func (engine *field) FromInterface(i interface{}) constraint.Element { b := utils.FromInterface(i) e.SetBigInt(&b) } - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) ToBigInt(c constraint.Element) *big.Int { +func (engine *field) ToBigInt(c constraint.U64) *big.Int { e := (*fr.Element)(c[:]) r := new(big.Int) e.BigInt(r) return r } -func (engine *field) Mul(a, b constraint.Element) constraint.Element { +func (engine *field) Mul(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Mul(_a, _b) return a } -func (engine *field) Add(a, b constraint.Element) constraint.Element { +func (engine *field) Add(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Add(_a, _b) return a } -func (engine *field) Sub(a, b constraint.Element) constraint.Element { +func (engine *field) Sub(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Sub(_a, _b) return a } -func (engine *field) Neg(a constraint.Element) constraint.Element { +func (engine *field) Neg(a constraint.U64) constraint.U64 { e := (*fr.Element)(a[:]) e.Neg(e) return a } -func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { +func (engine *field) Inverse(a constraint.U64) (constraint.U64, bool) { if a.IsZero() { return a, false } @@ -193,24 +194,24 @@ func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { return a, true } -func (engine *field) IsOne(a constraint.Element) bool { +func (engine *field) IsOne(a constraint.U64) bool { e := (*fr.Element)(a[:]) return e.IsOne() } -func (engine *field) One() constraint.Element { +func (engine *field) One() constraint.U64 { e := fr.One() - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) String(a constraint.Element) string { +func (engine *field) String(a constraint.U64) string { e := (*fr.Element)(a[:]) return e.String() } -func (engine *field) Uint64(a constraint.Element) (uint64, bool) { +func (engine *field) Uint64(a constraint.U64) (uint64, bool) { e := (*fr.Element)(a[:]) if !e.IsUint64() { return 0, false diff --git a/constraint/bls12-381/gkr.go b/constraint/bls12-381/gkr.go deleted file mode 100644 index acf57d9d..00000000 --- a/constraint/bls12-381/gkr.go +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2020-2025 Consensys Software Inc. -// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. - -// Code generated by gnark DO NOT EDIT - -package cs - -import ( - "fmt" - "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" - "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/gkr" - "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" - fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" - "github.com/consensys/gnark-crypto/utils" - "github.com/consensys/gnark/constraint" - hint "github.com/consensys/gnark/constraint/solver" - algo_utils "github.com/consensys/gnark/internal/utils" - "hash" - "math/big" - "sync" -) - -type GkrSolvingData struct { - assignments gkr.WireAssignment - circuit gkr.Circuit - memoryPool polynomial.Pool - workers *utils.WorkerPool -} - -func convertCircuit(noPtr constraint.GkrCircuit) (gkr.Circuit, error) { - resCircuit := make(gkr.Circuit, len(noPtr)) - var found bool - for i := range noPtr { - if resCircuit[i].Gate, found = gkr.Gates[noPtr[i].Gate]; !found && noPtr[i].Gate != "" { - return nil, fmt.Errorf("gate \"%s\" not found", noPtr[i].Gate) - } - resCircuit[i].Inputs = algo_utils.Map(noPtr[i].Inputs, algo_utils.SlicePtrAt(resCircuit)) - } - return resCircuit, nil -} - -func (d *GkrSolvingData) init(info constraint.GkrInfo) (assignment gkrAssignment, err error) { - if d.circuit, err = convertCircuit(info.Circuit); err != nil { - return - } - d.memoryPool = polynomial.NewPool(d.circuit.MemoryRequirements(info.NbInstances)...) - d.workers = utils.NewWorkerPool() - - assignment = make(gkrAssignment, len(d.circuit)) - d.assignments = make(gkr.WireAssignment, len(d.circuit)) - for i := range assignment { - assignment[i] = d.memoryPool.Make(info.NbInstances) - d.assignments[&d.circuit[i]] = assignment[i] - } - return -} - -func (d *GkrSolvingData) dumpAssignments() { - for _, p := range d.assignments { - d.memoryPool.Dump(p) - } -} - -// this module assumes that wire and instance indexes respect dependencies - -type gkrAssignment [][]fr.Element //gkrAssignment is indexed wire first, instance second - -func (a gkrAssignment) setOuts(circuit constraint.GkrCircuit, outs []*big.Int) { - outsI := 0 - for i := range circuit { - if circuit[i].IsOutput() { - for j := range a[i] { - a[i][j].BigInt(outs[outsI]) - outsI++ - } - } - } - // Check if outsI == len(outs)? -} - -func GkrSolveHint(info constraint.GkrInfo, solvingData *GkrSolvingData) hint.Hint { - return func(_ *big.Int, ins, outs []*big.Int) error { - // assumes assignmentVector is arranged wire first, instance second in order of solution - circuit := info.Circuit - nbInstances := info.NbInstances - offsets := info.AssignmentOffsets() - assignment, err := solvingData.init(info) - if err != nil { - return err - } - chunks := circuit.Chunks(nbInstances) - - solveTask := func(chunkOffset int) utils.Task { - return func(startInChunk, endInChunk int) { - start := startInChunk + chunkOffset - end := endInChunk + chunkOffset - inputs := solvingData.memoryPool.Make(info.MaxNIns) - dependencyHeads := make([]int, len(circuit)) - for wI, w := range circuit { - dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { - return w.Dependencies[i].InputInstance - }, len(w.Dependencies), start) - } - - for instanceI := start; instanceI < end; instanceI++ { - for wireI, wire := range circuit { - if wire.IsInput() { - if dependencyHeads[wireI] < len(wire.Dependencies) && instanceI == wire.Dependencies[dependencyHeads[wireI]].InputInstance { - dep := wire.Dependencies[dependencyHeads[wireI]] - assignment[wireI][instanceI].Set(&assignment[dep.OutputWire][dep.OutputInstance]) - dependencyHeads[wireI]++ - } else { - assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) - } - } else { - // assemble the inputs - inputIndexes := info.Circuit[wireI].Inputs - for i, inputI := range inputIndexes { - inputs[i].Set(&assignment[inputI][instanceI]) - } - gate := solvingData.circuit[wireI].Gate - assignment[wireI][instanceI] = gate.Evaluate(inputs[:len(inputIndexes)]...) - } - } - } - solvingData.memoryPool.Dump(inputs) - } - } - - start := 0 - for _, end := range chunks { - solvingData.workers.Submit(end-start, solveTask(start), 1024).Wait() - start = end - } - - assignment.setOuts(info.Circuit, outs) - - return nil - } -} - -func frToBigInts(dst []*big.Int, src []fr.Element) { - for i := range src { - src[i].BigInt(dst[i]) - } -} - -func GkrProveHint(hashName string, data *GkrSolvingData) hint.Hint { - - return func(_ *big.Int, ins, outs []*big.Int) error { - insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called - b := make([]byte, fr.Bytes) - i.FillBytes(b) - return b[:] - }) - - hsh, err := GetHashBuilder(hashName) - if err != nil { - return err - } - - proof, err := gkr.Prove(data.circuit, data.assignments, fiatshamir.WithHash(hsh(), insBytes...), gkr.WithPool(&data.memoryPool), gkr.WithWorkers(data.workers)) - if err != nil { - return err - } - - // serialize proof: TODO: In gnark-crypto? - offset := 0 - for i := range proof { - for _, poly := range proof[i].PartialSumPolys { - frToBigInts(outs[offset:], poly) - offset += len(poly) - } - if proof[i].FinalEvalProof != nil { - finalEvalProof := proof[i].FinalEvalProof.([]fr.Element) - frToBigInts(outs[offset:], finalEvalProof) - offset += len(finalEvalProof) - } - } - - data.dumpAssignments() - - return nil - - } -} - -// TODO: Move to gnark-crypto -var ( - hashBuilderRegistry = make(map[string]func() hash.Hash) - hasBuilderLock sync.RWMutex -) - -func RegisterHashBuilder(name string, builder func() hash.Hash) { - hasBuilderLock.Lock() - defer hasBuilderLock.Unlock() - hashBuilderRegistry[name] = builder -} - -func GetHashBuilder(name string) (func() hash.Hash, error) { - hasBuilderLock.RLock() - defer hasBuilderLock.RUnlock() - builder, ok := hashBuilderRegistry[name] - if !ok { - return nil, fmt.Errorf("hash function not found") - } - return builder, nil -} - -// For testing purposes -type ConstPseudoHash int - -func (c ConstPseudoHash) Write(p []byte) (int, error) { - return len(p), nil -} - -func (c ConstPseudoHash) Sum([]byte) []byte { - var x fr.Element - x.SetInt64(int64(c)) - res := x.Bytes() - return res[:] -} - -func (c ConstPseudoHash) Reset() {} - -func (c ConstPseudoHash) Size() int { - return fr.Bytes -} - -func (c ConstPseudoHash) BlockSize() int { - return fr.Bytes -} diff --git a/constraint/bls12-381/r1cs_test.go b/constraint/bls12-381/r1cs_test.go index 6a70cf6c..bea1fcc5 100644 --- a/constraint/bls12-381/r1cs_test.go +++ b/constraint/bls12-381/r1cs_test.go @@ -7,12 +7,14 @@ package cs_test import ( "bytes" + "reflect" + "testing" + + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/internal/backend/circuits" - "reflect" - "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -29,8 +31,9 @@ func TestSerialization(t *testing.T) { for name := range circuits.Circuits { t.Run(name, func(t *testing.T) { tc := circuits.Circuits[name] + builder := r1cs.NewBuilder[constraint.U64] - r1cs1, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs1, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -39,7 +42,7 @@ func TestSerialization(t *testing.T) { } // compile a second time to ensure determinism - r1cs2, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs2, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -146,7 +149,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("scs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), scs.NewBuilder, &c) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), scs.NewBuilder, &c) if err != nil { b.Fatal(err) } @@ -160,7 +163,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("r1cs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) if err != nil { b.Fatal(err) } diff --git a/constraint/bls12-381/solver.go b/constraint/bls12-381/solver.go index 6ec5f8dd..1bfa4c58 100644 --- a/constraint/bls12-381/solver.go +++ b/constraint/bls12-381/solver.go @@ -8,11 +8,6 @@ package cs import ( "errors" "fmt" - "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/field/pool" - "github.com/consensys/gnark/constraint" - csolver "github.com/consensys/gnark/constraint/solver" - "github.com/rs/zerolog" "math" "math/big" "strconv" @@ -20,6 +15,15 @@ import ( "sync" "sync/atomic" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/pool" + "github.com/consensys/gnark/constraint" + csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/constraint/solver/gkrgates" + gkr "github.com/consensys/gnark/internal/gkr/bls12-381" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/rs/zerolog" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" ) @@ -47,10 +51,14 @@ type solver struct { func newSolver(cs *system, witness fr.Vector, opts ...csolver.Option) (*solver, error) { // add GKR options to overwrite the placeholder if cs.GkrInfo.Is() { - var gkrData GkrSolvingData + var gkrData gkr.SolvingData + solvingInfo, err := gkrtypes.StoringToSolvingInfo(cs.GkrInfo, gkrgates.Get) + if err != nil { + return nil, err + } opts = append(opts, - csolver.OverrideHint(cs.GkrInfo.SolveHintID, GkrSolveHint(cs.GkrInfo, &gkrData)), - csolver.OverrideHint(cs.GkrInfo.ProveHintID, GkrProveHint(cs.GkrInfo.HashName, &gkrData))) + csolver.OverrideHint(cs.GkrInfo.SolveHintID, gkr.SolveHint(solvingInfo, &gkrData)), + csolver.OverrideHint(cs.GkrInfo.ProveHintID, gkr.ProveHint(cs.GkrInfo.HashName, &gkrData))) } // parse options opt, err := csolver.NewConfig(opts...) @@ -332,18 +340,18 @@ func (solver *solver) divByCoeff(res *fr.Element, cID uint32) { } // Implement constraint.Solver -func (s *solver) GetValue(cID, vID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetValue(cID, vID uint32) constraint.U64 { + var r constraint.U64 e := s.computeTerm(constraint.Term{CID: cID, VID: vID}) copy(r[:], e[:]) return r } -func (s *solver) GetCoeff(cID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetCoeff(cID uint32) constraint.U64 { + var r constraint.U64 copy(r[:], s.Coefficients[cID][:]) return r } -func (s *solver) SetValue(vID uint32, f constraint.Element) { +func (s *solver) SetValue(vID uint32, f constraint.U64) { s.set(int(vID), *(*fr.Element)(f[:])) } @@ -353,7 +361,7 @@ func (s *solver) IsSolved(vID uint32) bool { // Read interprets input calldata as either a LinearExpression (if R1CS) or a Term (if Plonkish), // evaluates it and return the result and the number of uint32 word read. -func (s *solver) Read(calldata []uint32) (constraint.Element, int) { +func (s *solver) Read(calldata []uint32) (constraint.U64, int) { if s.Type == constraint.SystemSparseR1CS { if calldata[0] != 1 { panic("invalid calldata") @@ -369,7 +377,7 @@ func (s *solver) Read(calldata []uint32) (constraint.Element, int) { j += 2 } - var ret constraint.Element + var ret constraint.U64 copy(ret[:], r[:]) return ret, j } @@ -393,7 +401,7 @@ func (solver *solver) processInstruction(pi constraint.PackedInstruction, scratc } // blueprint declared "I know how to solve this." - if bc, ok := blueprint.(constraint.BlueprintSolvable); ok { + if bc, ok := blueprint.(constraint.BlueprintSolvable[constraint.U64]); ok { if err := bc.Solve(solver, inst); err != nil { return solver.wrapErrWithDebugInfo(cID, err) } diff --git a/constraint/bls12-381/system.go b/constraint/bls12-381/system.go index e4417923..3758d6c9 100644 --- a/constraint/bls12-381/system.go +++ b/constraint/bls12-381/system.go @@ -12,6 +12,7 @@ import ( "github.com/consensys/gnark/backend/witness" "github.com/consensys/gnark/constraint" csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/internal/gkr/gkrinfo" "github.com/consensys/gnark/logger" "github.com/consensys/gnark-crypto/ecc" @@ -66,7 +67,7 @@ func (cs *system) Solve(witness witness.Witness, opts ...csolver.Option) (any, e // reset the stateful blueprints for i := range cs.Blueprints { - if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful); ok { + if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful[constraint.U64]); ok { b.Reset() } } @@ -135,7 +136,7 @@ func (cs *system) CurveID() ecc.ID { return ecc.BLS12_381 } -func (cs *system) GetCoefficient(i int) (r constraint.Element) { +func (cs *system) GetCoefficient(i int) (r constraint.U64) { copy(r[:], cs.Coefficients[i][:]) return } @@ -288,6 +289,6 @@ func (t *SparseR1CSSolution) ReadFrom(r io.Reader) (int64, error) { return n, err } -func (s *system) AddGkr(gkr constraint.GkrInfo) error { +func (s *system) AddGkr(gkr gkrinfo.StoringInfo) error { return s.System.AddGkr(gkr) } diff --git a/constraint/bls24-315/coeff.go b/constraint/bls24-315/coeff.go index 39bb5b34..58619e35 100644 --- a/constraint/bls24-315/coeff.go +++ b/constraint/bls24-315/coeff.go @@ -8,9 +8,10 @@ package cs import ( "encoding/binary" "errors" + "math/big" + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/utils" - "math/big" "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" ) @@ -73,7 +74,7 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { return nil } -func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { +func (ct *CoeffTable) AddCoeff(coeff constraint.U64) uint32 { c := (*fr.Element)(coeff[:]) var cID uint32 if c.IsZero() { @@ -99,7 +100,7 @@ func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { return cID } -func (ct *CoeffTable) MakeTerm(coeff constraint.Element, variableID int) constraint.Term { +func (ct *CoeffTable) MakeTerm(coeff constraint.U64, variableID int) constraint.Term { cID := ct.AddCoeff(coeff) return constraint.Term{VID: uint32(variableID), CID: cID} } @@ -112,7 +113,7 @@ func (ct *CoeffTable) CoeffToString(cID int) string { // implements constraint.Field type field struct{} -var _ constraint.Field = &field{} +var _ constraint.Field[constraint.U64] = &field{} var ( two fr.Element @@ -128,7 +129,7 @@ func init() { minusTwo.Neg(&two) } -func (engine *field) FromInterface(i interface{}) constraint.Element { +func (engine *field) FromInterface(i interface{}) constraint.U64 { var e fr.Element if _, err := e.SetInterface(i); err != nil { // need to clean that --> some code path are dissimilar @@ -137,43 +138,43 @@ func (engine *field) FromInterface(i interface{}) constraint.Element { b := utils.FromInterface(i) e.SetBigInt(&b) } - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) ToBigInt(c constraint.Element) *big.Int { +func (engine *field) ToBigInt(c constraint.U64) *big.Int { e := (*fr.Element)(c[:]) r := new(big.Int) e.BigInt(r) return r } -func (engine *field) Mul(a, b constraint.Element) constraint.Element { +func (engine *field) Mul(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Mul(_a, _b) return a } -func (engine *field) Add(a, b constraint.Element) constraint.Element { +func (engine *field) Add(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Add(_a, _b) return a } -func (engine *field) Sub(a, b constraint.Element) constraint.Element { +func (engine *field) Sub(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Sub(_a, _b) return a } -func (engine *field) Neg(a constraint.Element) constraint.Element { +func (engine *field) Neg(a constraint.U64) constraint.U64 { e := (*fr.Element)(a[:]) e.Neg(e) return a } -func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { +func (engine *field) Inverse(a constraint.U64) (constraint.U64, bool) { if a.IsZero() { return a, false } @@ -193,24 +194,24 @@ func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { return a, true } -func (engine *field) IsOne(a constraint.Element) bool { +func (engine *field) IsOne(a constraint.U64) bool { e := (*fr.Element)(a[:]) return e.IsOne() } -func (engine *field) One() constraint.Element { +func (engine *field) One() constraint.U64 { e := fr.One() - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) String(a constraint.Element) string { +func (engine *field) String(a constraint.U64) string { e := (*fr.Element)(a[:]) return e.String() } -func (engine *field) Uint64(a constraint.Element) (uint64, bool) { +func (engine *field) Uint64(a constraint.U64) (uint64, bool) { e := (*fr.Element)(a[:]) if !e.IsUint64() { return 0, false diff --git a/constraint/bls24-315/gkr.go b/constraint/bls24-315/gkr.go deleted file mode 100644 index e39d7447..00000000 --- a/constraint/bls24-315/gkr.go +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2020-2025 Consensys Software Inc. -// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. - -// Code generated by gnark DO NOT EDIT - -package cs - -import ( - "fmt" - "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" - "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/gkr" - "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/polynomial" - fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" - "github.com/consensys/gnark-crypto/utils" - "github.com/consensys/gnark/constraint" - hint "github.com/consensys/gnark/constraint/solver" - algo_utils "github.com/consensys/gnark/internal/utils" - "hash" - "math/big" - "sync" -) - -type GkrSolvingData struct { - assignments gkr.WireAssignment - circuit gkr.Circuit - memoryPool polynomial.Pool - workers *utils.WorkerPool -} - -func convertCircuit(noPtr constraint.GkrCircuit) (gkr.Circuit, error) { - resCircuit := make(gkr.Circuit, len(noPtr)) - var found bool - for i := range noPtr { - if resCircuit[i].Gate, found = gkr.Gates[noPtr[i].Gate]; !found && noPtr[i].Gate != "" { - return nil, fmt.Errorf("gate \"%s\" not found", noPtr[i].Gate) - } - resCircuit[i].Inputs = algo_utils.Map(noPtr[i].Inputs, algo_utils.SlicePtrAt(resCircuit)) - } - return resCircuit, nil -} - -func (d *GkrSolvingData) init(info constraint.GkrInfo) (assignment gkrAssignment, err error) { - if d.circuit, err = convertCircuit(info.Circuit); err != nil { - return - } - d.memoryPool = polynomial.NewPool(d.circuit.MemoryRequirements(info.NbInstances)...) - d.workers = utils.NewWorkerPool() - - assignment = make(gkrAssignment, len(d.circuit)) - d.assignments = make(gkr.WireAssignment, len(d.circuit)) - for i := range assignment { - assignment[i] = d.memoryPool.Make(info.NbInstances) - d.assignments[&d.circuit[i]] = assignment[i] - } - return -} - -func (d *GkrSolvingData) dumpAssignments() { - for _, p := range d.assignments { - d.memoryPool.Dump(p) - } -} - -// this module assumes that wire and instance indexes respect dependencies - -type gkrAssignment [][]fr.Element //gkrAssignment is indexed wire first, instance second - -func (a gkrAssignment) setOuts(circuit constraint.GkrCircuit, outs []*big.Int) { - outsI := 0 - for i := range circuit { - if circuit[i].IsOutput() { - for j := range a[i] { - a[i][j].BigInt(outs[outsI]) - outsI++ - } - } - } - // Check if outsI == len(outs)? -} - -func GkrSolveHint(info constraint.GkrInfo, solvingData *GkrSolvingData) hint.Hint { - return func(_ *big.Int, ins, outs []*big.Int) error { - // assumes assignmentVector is arranged wire first, instance second in order of solution - circuit := info.Circuit - nbInstances := info.NbInstances - offsets := info.AssignmentOffsets() - assignment, err := solvingData.init(info) - if err != nil { - return err - } - chunks := circuit.Chunks(nbInstances) - - solveTask := func(chunkOffset int) utils.Task { - return func(startInChunk, endInChunk int) { - start := startInChunk + chunkOffset - end := endInChunk + chunkOffset - inputs := solvingData.memoryPool.Make(info.MaxNIns) - dependencyHeads := make([]int, len(circuit)) - for wI, w := range circuit { - dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { - return w.Dependencies[i].InputInstance - }, len(w.Dependencies), start) - } - - for instanceI := start; instanceI < end; instanceI++ { - for wireI, wire := range circuit { - if wire.IsInput() { - if dependencyHeads[wireI] < len(wire.Dependencies) && instanceI == wire.Dependencies[dependencyHeads[wireI]].InputInstance { - dep := wire.Dependencies[dependencyHeads[wireI]] - assignment[wireI][instanceI].Set(&assignment[dep.OutputWire][dep.OutputInstance]) - dependencyHeads[wireI]++ - } else { - assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) - } - } else { - // assemble the inputs - inputIndexes := info.Circuit[wireI].Inputs - for i, inputI := range inputIndexes { - inputs[i].Set(&assignment[inputI][instanceI]) - } - gate := solvingData.circuit[wireI].Gate - assignment[wireI][instanceI] = gate.Evaluate(inputs[:len(inputIndexes)]...) - } - } - } - solvingData.memoryPool.Dump(inputs) - } - } - - start := 0 - for _, end := range chunks { - solvingData.workers.Submit(end-start, solveTask(start), 1024).Wait() - start = end - } - - assignment.setOuts(info.Circuit, outs) - - return nil - } -} - -func frToBigInts(dst []*big.Int, src []fr.Element) { - for i := range src { - src[i].BigInt(dst[i]) - } -} - -func GkrProveHint(hashName string, data *GkrSolvingData) hint.Hint { - - return func(_ *big.Int, ins, outs []*big.Int) error { - insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called - b := make([]byte, fr.Bytes) - i.FillBytes(b) - return b[:] - }) - - hsh, err := GetHashBuilder(hashName) - if err != nil { - return err - } - - proof, err := gkr.Prove(data.circuit, data.assignments, fiatshamir.WithHash(hsh(), insBytes...), gkr.WithPool(&data.memoryPool), gkr.WithWorkers(data.workers)) - if err != nil { - return err - } - - // serialize proof: TODO: In gnark-crypto? - offset := 0 - for i := range proof { - for _, poly := range proof[i].PartialSumPolys { - frToBigInts(outs[offset:], poly) - offset += len(poly) - } - if proof[i].FinalEvalProof != nil { - finalEvalProof := proof[i].FinalEvalProof.([]fr.Element) - frToBigInts(outs[offset:], finalEvalProof) - offset += len(finalEvalProof) - } - } - - data.dumpAssignments() - - return nil - - } -} - -// TODO: Move to gnark-crypto -var ( - hashBuilderRegistry = make(map[string]func() hash.Hash) - hasBuilderLock sync.RWMutex -) - -func RegisterHashBuilder(name string, builder func() hash.Hash) { - hasBuilderLock.Lock() - defer hasBuilderLock.Unlock() - hashBuilderRegistry[name] = builder -} - -func GetHashBuilder(name string) (func() hash.Hash, error) { - hasBuilderLock.RLock() - defer hasBuilderLock.RUnlock() - builder, ok := hashBuilderRegistry[name] - if !ok { - return nil, fmt.Errorf("hash function not found") - } - return builder, nil -} - -// For testing purposes -type ConstPseudoHash int - -func (c ConstPseudoHash) Write(p []byte) (int, error) { - return len(p), nil -} - -func (c ConstPseudoHash) Sum([]byte) []byte { - var x fr.Element - x.SetInt64(int64(c)) - res := x.Bytes() - return res[:] -} - -func (c ConstPseudoHash) Reset() {} - -func (c ConstPseudoHash) Size() int { - return fr.Bytes -} - -func (c ConstPseudoHash) BlockSize() int { - return fr.Bytes -} diff --git a/constraint/bls24-315/r1cs_test.go b/constraint/bls24-315/r1cs_test.go index da9bc60f..4f33de05 100644 --- a/constraint/bls24-315/r1cs_test.go +++ b/constraint/bls24-315/r1cs_test.go @@ -7,12 +7,14 @@ package cs_test import ( "bytes" + "reflect" + "testing" + + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/internal/backend/circuits" - "reflect" - "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -29,8 +31,9 @@ func TestSerialization(t *testing.T) { for name := range circuits.Circuits { t.Run(name, func(t *testing.T) { tc := circuits.Circuits[name] + builder := r1cs.NewBuilder[constraint.U64] - r1cs1, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs1, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -39,7 +42,7 @@ func TestSerialization(t *testing.T) { } // compile a second time to ensure determinism - r1cs2, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs2, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -146,7 +149,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("scs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), scs.NewBuilder, &c) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), scs.NewBuilder, &c) if err != nil { b.Fatal(err) } @@ -160,7 +163,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("r1cs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) if err != nil { b.Fatal(err) } diff --git a/constraint/bls24-315/solver.go b/constraint/bls24-315/solver.go index de3296dd..4f5b72c7 100644 --- a/constraint/bls24-315/solver.go +++ b/constraint/bls24-315/solver.go @@ -8,11 +8,6 @@ package cs import ( "errors" "fmt" - "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/field/pool" - "github.com/consensys/gnark/constraint" - csolver "github.com/consensys/gnark/constraint/solver" - "github.com/rs/zerolog" "math" "math/big" "strconv" @@ -20,6 +15,15 @@ import ( "sync" "sync/atomic" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/pool" + "github.com/consensys/gnark/constraint" + csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/constraint/solver/gkrgates" + gkr "github.com/consensys/gnark/internal/gkr/bls24-315" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/rs/zerolog" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" ) @@ -47,10 +51,14 @@ type solver struct { func newSolver(cs *system, witness fr.Vector, opts ...csolver.Option) (*solver, error) { // add GKR options to overwrite the placeholder if cs.GkrInfo.Is() { - var gkrData GkrSolvingData + var gkrData gkr.SolvingData + solvingInfo, err := gkrtypes.StoringToSolvingInfo(cs.GkrInfo, gkrgates.Get) + if err != nil { + return nil, err + } opts = append(opts, - csolver.OverrideHint(cs.GkrInfo.SolveHintID, GkrSolveHint(cs.GkrInfo, &gkrData)), - csolver.OverrideHint(cs.GkrInfo.ProveHintID, GkrProveHint(cs.GkrInfo.HashName, &gkrData))) + csolver.OverrideHint(cs.GkrInfo.SolveHintID, gkr.SolveHint(solvingInfo, &gkrData)), + csolver.OverrideHint(cs.GkrInfo.ProveHintID, gkr.ProveHint(cs.GkrInfo.HashName, &gkrData))) } // parse options opt, err := csolver.NewConfig(opts...) @@ -332,18 +340,18 @@ func (solver *solver) divByCoeff(res *fr.Element, cID uint32) { } // Implement constraint.Solver -func (s *solver) GetValue(cID, vID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetValue(cID, vID uint32) constraint.U64 { + var r constraint.U64 e := s.computeTerm(constraint.Term{CID: cID, VID: vID}) copy(r[:], e[:]) return r } -func (s *solver) GetCoeff(cID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetCoeff(cID uint32) constraint.U64 { + var r constraint.U64 copy(r[:], s.Coefficients[cID][:]) return r } -func (s *solver) SetValue(vID uint32, f constraint.Element) { +func (s *solver) SetValue(vID uint32, f constraint.U64) { s.set(int(vID), *(*fr.Element)(f[:])) } @@ -353,7 +361,7 @@ func (s *solver) IsSolved(vID uint32) bool { // Read interprets input calldata as either a LinearExpression (if R1CS) or a Term (if Plonkish), // evaluates it and return the result and the number of uint32 word read. -func (s *solver) Read(calldata []uint32) (constraint.Element, int) { +func (s *solver) Read(calldata []uint32) (constraint.U64, int) { if s.Type == constraint.SystemSparseR1CS { if calldata[0] != 1 { panic("invalid calldata") @@ -369,7 +377,7 @@ func (s *solver) Read(calldata []uint32) (constraint.Element, int) { j += 2 } - var ret constraint.Element + var ret constraint.U64 copy(ret[:], r[:]) return ret, j } @@ -393,7 +401,7 @@ func (solver *solver) processInstruction(pi constraint.PackedInstruction, scratc } // blueprint declared "I know how to solve this." - if bc, ok := blueprint.(constraint.BlueprintSolvable); ok { + if bc, ok := blueprint.(constraint.BlueprintSolvable[constraint.U64]); ok { if err := bc.Solve(solver, inst); err != nil { return solver.wrapErrWithDebugInfo(cID, err) } diff --git a/constraint/bls24-315/system.go b/constraint/bls24-315/system.go index fc302c75..13e66c3c 100644 --- a/constraint/bls24-315/system.go +++ b/constraint/bls24-315/system.go @@ -12,6 +12,7 @@ import ( "github.com/consensys/gnark/backend/witness" "github.com/consensys/gnark/constraint" csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/internal/gkr/gkrinfo" "github.com/consensys/gnark/logger" "github.com/consensys/gnark-crypto/ecc" @@ -66,7 +67,7 @@ func (cs *system) Solve(witness witness.Witness, opts ...csolver.Option) (any, e // reset the stateful blueprints for i := range cs.Blueprints { - if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful); ok { + if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful[constraint.U64]); ok { b.Reset() } } @@ -135,7 +136,7 @@ func (cs *system) CurveID() ecc.ID { return ecc.BLS24_315 } -func (cs *system) GetCoefficient(i int) (r constraint.Element) { +func (cs *system) GetCoefficient(i int) (r constraint.U64) { copy(r[:], cs.Coefficients[i][:]) return } @@ -288,6 +289,6 @@ func (t *SparseR1CSSolution) ReadFrom(r io.Reader) (int64, error) { return n, err } -func (s *system) AddGkr(gkr constraint.GkrInfo) error { +func (s *system) AddGkr(gkr gkrinfo.StoringInfo) error { return s.System.AddGkr(gkr) } diff --git a/constraint/bls24-317/coeff.go b/constraint/bls24-317/coeff.go index b9ca9a66..de0354d1 100644 --- a/constraint/bls24-317/coeff.go +++ b/constraint/bls24-317/coeff.go @@ -8,9 +8,10 @@ package cs import ( "encoding/binary" "errors" + "math/big" + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/utils" - "math/big" "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" ) @@ -73,7 +74,7 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { return nil } -func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { +func (ct *CoeffTable) AddCoeff(coeff constraint.U64) uint32 { c := (*fr.Element)(coeff[:]) var cID uint32 if c.IsZero() { @@ -99,7 +100,7 @@ func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { return cID } -func (ct *CoeffTable) MakeTerm(coeff constraint.Element, variableID int) constraint.Term { +func (ct *CoeffTable) MakeTerm(coeff constraint.U64, variableID int) constraint.Term { cID := ct.AddCoeff(coeff) return constraint.Term{VID: uint32(variableID), CID: cID} } @@ -112,7 +113,7 @@ func (ct *CoeffTable) CoeffToString(cID int) string { // implements constraint.Field type field struct{} -var _ constraint.Field = &field{} +var _ constraint.Field[constraint.U64] = &field{} var ( two fr.Element @@ -128,7 +129,7 @@ func init() { minusTwo.Neg(&two) } -func (engine *field) FromInterface(i interface{}) constraint.Element { +func (engine *field) FromInterface(i interface{}) constraint.U64 { var e fr.Element if _, err := e.SetInterface(i); err != nil { // need to clean that --> some code path are dissimilar @@ -137,43 +138,43 @@ func (engine *field) FromInterface(i interface{}) constraint.Element { b := utils.FromInterface(i) e.SetBigInt(&b) } - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) ToBigInt(c constraint.Element) *big.Int { +func (engine *field) ToBigInt(c constraint.U64) *big.Int { e := (*fr.Element)(c[:]) r := new(big.Int) e.BigInt(r) return r } -func (engine *field) Mul(a, b constraint.Element) constraint.Element { +func (engine *field) Mul(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Mul(_a, _b) return a } -func (engine *field) Add(a, b constraint.Element) constraint.Element { +func (engine *field) Add(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Add(_a, _b) return a } -func (engine *field) Sub(a, b constraint.Element) constraint.Element { +func (engine *field) Sub(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Sub(_a, _b) return a } -func (engine *field) Neg(a constraint.Element) constraint.Element { +func (engine *field) Neg(a constraint.U64) constraint.U64 { e := (*fr.Element)(a[:]) e.Neg(e) return a } -func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { +func (engine *field) Inverse(a constraint.U64) (constraint.U64, bool) { if a.IsZero() { return a, false } @@ -193,24 +194,24 @@ func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { return a, true } -func (engine *field) IsOne(a constraint.Element) bool { +func (engine *field) IsOne(a constraint.U64) bool { e := (*fr.Element)(a[:]) return e.IsOne() } -func (engine *field) One() constraint.Element { +func (engine *field) One() constraint.U64 { e := fr.One() - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) String(a constraint.Element) string { +func (engine *field) String(a constraint.U64) string { e := (*fr.Element)(a[:]) return e.String() } -func (engine *field) Uint64(a constraint.Element) (uint64, bool) { +func (engine *field) Uint64(a constraint.U64) (uint64, bool) { e := (*fr.Element)(a[:]) if !e.IsUint64() { return 0, false diff --git a/constraint/bls24-317/gkr.go b/constraint/bls24-317/gkr.go deleted file mode 100644 index 76d080a4..00000000 --- a/constraint/bls24-317/gkr.go +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2020-2025 Consensys Software Inc. -// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. - -// Code generated by gnark DO NOT EDIT - -package cs - -import ( - "fmt" - "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" - "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/gkr" - "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/polynomial" - fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" - "github.com/consensys/gnark-crypto/utils" - "github.com/consensys/gnark/constraint" - hint "github.com/consensys/gnark/constraint/solver" - algo_utils "github.com/consensys/gnark/internal/utils" - "hash" - "math/big" - "sync" -) - -type GkrSolvingData struct { - assignments gkr.WireAssignment - circuit gkr.Circuit - memoryPool polynomial.Pool - workers *utils.WorkerPool -} - -func convertCircuit(noPtr constraint.GkrCircuit) (gkr.Circuit, error) { - resCircuit := make(gkr.Circuit, len(noPtr)) - var found bool - for i := range noPtr { - if resCircuit[i].Gate, found = gkr.Gates[noPtr[i].Gate]; !found && noPtr[i].Gate != "" { - return nil, fmt.Errorf("gate \"%s\" not found", noPtr[i].Gate) - } - resCircuit[i].Inputs = algo_utils.Map(noPtr[i].Inputs, algo_utils.SlicePtrAt(resCircuit)) - } - return resCircuit, nil -} - -func (d *GkrSolvingData) init(info constraint.GkrInfo) (assignment gkrAssignment, err error) { - if d.circuit, err = convertCircuit(info.Circuit); err != nil { - return - } - d.memoryPool = polynomial.NewPool(d.circuit.MemoryRequirements(info.NbInstances)...) - d.workers = utils.NewWorkerPool() - - assignment = make(gkrAssignment, len(d.circuit)) - d.assignments = make(gkr.WireAssignment, len(d.circuit)) - for i := range assignment { - assignment[i] = d.memoryPool.Make(info.NbInstances) - d.assignments[&d.circuit[i]] = assignment[i] - } - return -} - -func (d *GkrSolvingData) dumpAssignments() { - for _, p := range d.assignments { - d.memoryPool.Dump(p) - } -} - -// this module assumes that wire and instance indexes respect dependencies - -type gkrAssignment [][]fr.Element //gkrAssignment is indexed wire first, instance second - -func (a gkrAssignment) setOuts(circuit constraint.GkrCircuit, outs []*big.Int) { - outsI := 0 - for i := range circuit { - if circuit[i].IsOutput() { - for j := range a[i] { - a[i][j].BigInt(outs[outsI]) - outsI++ - } - } - } - // Check if outsI == len(outs)? -} - -func GkrSolveHint(info constraint.GkrInfo, solvingData *GkrSolvingData) hint.Hint { - return func(_ *big.Int, ins, outs []*big.Int) error { - // assumes assignmentVector is arranged wire first, instance second in order of solution - circuit := info.Circuit - nbInstances := info.NbInstances - offsets := info.AssignmentOffsets() - assignment, err := solvingData.init(info) - if err != nil { - return err - } - chunks := circuit.Chunks(nbInstances) - - solveTask := func(chunkOffset int) utils.Task { - return func(startInChunk, endInChunk int) { - start := startInChunk + chunkOffset - end := endInChunk + chunkOffset - inputs := solvingData.memoryPool.Make(info.MaxNIns) - dependencyHeads := make([]int, len(circuit)) - for wI, w := range circuit { - dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { - return w.Dependencies[i].InputInstance - }, len(w.Dependencies), start) - } - - for instanceI := start; instanceI < end; instanceI++ { - for wireI, wire := range circuit { - if wire.IsInput() { - if dependencyHeads[wireI] < len(wire.Dependencies) && instanceI == wire.Dependencies[dependencyHeads[wireI]].InputInstance { - dep := wire.Dependencies[dependencyHeads[wireI]] - assignment[wireI][instanceI].Set(&assignment[dep.OutputWire][dep.OutputInstance]) - dependencyHeads[wireI]++ - } else { - assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) - } - } else { - // assemble the inputs - inputIndexes := info.Circuit[wireI].Inputs - for i, inputI := range inputIndexes { - inputs[i].Set(&assignment[inputI][instanceI]) - } - gate := solvingData.circuit[wireI].Gate - assignment[wireI][instanceI] = gate.Evaluate(inputs[:len(inputIndexes)]...) - } - } - } - solvingData.memoryPool.Dump(inputs) - } - } - - start := 0 - for _, end := range chunks { - solvingData.workers.Submit(end-start, solveTask(start), 1024).Wait() - start = end - } - - assignment.setOuts(info.Circuit, outs) - - return nil - } -} - -func frToBigInts(dst []*big.Int, src []fr.Element) { - for i := range src { - src[i].BigInt(dst[i]) - } -} - -func GkrProveHint(hashName string, data *GkrSolvingData) hint.Hint { - - return func(_ *big.Int, ins, outs []*big.Int) error { - insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called - b := make([]byte, fr.Bytes) - i.FillBytes(b) - return b[:] - }) - - hsh, err := GetHashBuilder(hashName) - if err != nil { - return err - } - - proof, err := gkr.Prove(data.circuit, data.assignments, fiatshamir.WithHash(hsh(), insBytes...), gkr.WithPool(&data.memoryPool), gkr.WithWorkers(data.workers)) - if err != nil { - return err - } - - // serialize proof: TODO: In gnark-crypto? - offset := 0 - for i := range proof { - for _, poly := range proof[i].PartialSumPolys { - frToBigInts(outs[offset:], poly) - offset += len(poly) - } - if proof[i].FinalEvalProof != nil { - finalEvalProof := proof[i].FinalEvalProof.([]fr.Element) - frToBigInts(outs[offset:], finalEvalProof) - offset += len(finalEvalProof) - } - } - - data.dumpAssignments() - - return nil - - } -} - -// TODO: Move to gnark-crypto -var ( - hashBuilderRegistry = make(map[string]func() hash.Hash) - hasBuilderLock sync.RWMutex -) - -func RegisterHashBuilder(name string, builder func() hash.Hash) { - hasBuilderLock.Lock() - defer hasBuilderLock.Unlock() - hashBuilderRegistry[name] = builder -} - -func GetHashBuilder(name string) (func() hash.Hash, error) { - hasBuilderLock.RLock() - defer hasBuilderLock.RUnlock() - builder, ok := hashBuilderRegistry[name] - if !ok { - return nil, fmt.Errorf("hash function not found") - } - return builder, nil -} - -// For testing purposes -type ConstPseudoHash int - -func (c ConstPseudoHash) Write(p []byte) (int, error) { - return len(p), nil -} - -func (c ConstPseudoHash) Sum([]byte) []byte { - var x fr.Element - x.SetInt64(int64(c)) - res := x.Bytes() - return res[:] -} - -func (c ConstPseudoHash) Reset() {} - -func (c ConstPseudoHash) Size() int { - return fr.Bytes -} - -func (c ConstPseudoHash) BlockSize() int { - return fr.Bytes -} diff --git a/constraint/bls24-317/r1cs_test.go b/constraint/bls24-317/r1cs_test.go index d891fc93..40808c22 100644 --- a/constraint/bls24-317/r1cs_test.go +++ b/constraint/bls24-317/r1cs_test.go @@ -7,12 +7,14 @@ package cs_test import ( "bytes" + "reflect" + "testing" + + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/internal/backend/circuits" - "reflect" - "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -29,8 +31,9 @@ func TestSerialization(t *testing.T) { for name := range circuits.Circuits { t.Run(name, func(t *testing.T) { tc := circuits.Circuits[name] + builder := r1cs.NewBuilder[constraint.U64] - r1cs1, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs1, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -39,7 +42,7 @@ func TestSerialization(t *testing.T) { } // compile a second time to ensure determinism - r1cs2, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs2, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -146,7 +149,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("scs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), scs.NewBuilder, &c) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), scs.NewBuilder, &c) if err != nil { b.Fatal(err) } @@ -160,7 +163,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("r1cs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) if err != nil { b.Fatal(err) } diff --git a/constraint/bls24-317/solver.go b/constraint/bls24-317/solver.go index 577c83bd..9462b5d3 100644 --- a/constraint/bls24-317/solver.go +++ b/constraint/bls24-317/solver.go @@ -8,11 +8,6 @@ package cs import ( "errors" "fmt" - "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/field/pool" - "github.com/consensys/gnark/constraint" - csolver "github.com/consensys/gnark/constraint/solver" - "github.com/rs/zerolog" "math" "math/big" "strconv" @@ -20,6 +15,15 @@ import ( "sync" "sync/atomic" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/pool" + "github.com/consensys/gnark/constraint" + csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/constraint/solver/gkrgates" + gkr "github.com/consensys/gnark/internal/gkr/bls24-317" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/rs/zerolog" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" ) @@ -47,10 +51,14 @@ type solver struct { func newSolver(cs *system, witness fr.Vector, opts ...csolver.Option) (*solver, error) { // add GKR options to overwrite the placeholder if cs.GkrInfo.Is() { - var gkrData GkrSolvingData + var gkrData gkr.SolvingData + solvingInfo, err := gkrtypes.StoringToSolvingInfo(cs.GkrInfo, gkrgates.Get) + if err != nil { + return nil, err + } opts = append(opts, - csolver.OverrideHint(cs.GkrInfo.SolveHintID, GkrSolveHint(cs.GkrInfo, &gkrData)), - csolver.OverrideHint(cs.GkrInfo.ProveHintID, GkrProveHint(cs.GkrInfo.HashName, &gkrData))) + csolver.OverrideHint(cs.GkrInfo.SolveHintID, gkr.SolveHint(solvingInfo, &gkrData)), + csolver.OverrideHint(cs.GkrInfo.ProveHintID, gkr.ProveHint(cs.GkrInfo.HashName, &gkrData))) } // parse options opt, err := csolver.NewConfig(opts...) @@ -332,18 +340,18 @@ func (solver *solver) divByCoeff(res *fr.Element, cID uint32) { } // Implement constraint.Solver -func (s *solver) GetValue(cID, vID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetValue(cID, vID uint32) constraint.U64 { + var r constraint.U64 e := s.computeTerm(constraint.Term{CID: cID, VID: vID}) copy(r[:], e[:]) return r } -func (s *solver) GetCoeff(cID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetCoeff(cID uint32) constraint.U64 { + var r constraint.U64 copy(r[:], s.Coefficients[cID][:]) return r } -func (s *solver) SetValue(vID uint32, f constraint.Element) { +func (s *solver) SetValue(vID uint32, f constraint.U64) { s.set(int(vID), *(*fr.Element)(f[:])) } @@ -353,7 +361,7 @@ func (s *solver) IsSolved(vID uint32) bool { // Read interprets input calldata as either a LinearExpression (if R1CS) or a Term (if Plonkish), // evaluates it and return the result and the number of uint32 word read. -func (s *solver) Read(calldata []uint32) (constraint.Element, int) { +func (s *solver) Read(calldata []uint32) (constraint.U64, int) { if s.Type == constraint.SystemSparseR1CS { if calldata[0] != 1 { panic("invalid calldata") @@ -369,7 +377,7 @@ func (s *solver) Read(calldata []uint32) (constraint.Element, int) { j += 2 } - var ret constraint.Element + var ret constraint.U64 copy(ret[:], r[:]) return ret, j } @@ -393,7 +401,7 @@ func (solver *solver) processInstruction(pi constraint.PackedInstruction, scratc } // blueprint declared "I know how to solve this." - if bc, ok := blueprint.(constraint.BlueprintSolvable); ok { + if bc, ok := blueprint.(constraint.BlueprintSolvable[constraint.U64]); ok { if err := bc.Solve(solver, inst); err != nil { return solver.wrapErrWithDebugInfo(cID, err) } diff --git a/constraint/bls24-317/system.go b/constraint/bls24-317/system.go index f1683111..deaa1bf6 100644 --- a/constraint/bls24-317/system.go +++ b/constraint/bls24-317/system.go @@ -12,6 +12,7 @@ import ( "github.com/consensys/gnark/backend/witness" "github.com/consensys/gnark/constraint" csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/internal/gkr/gkrinfo" "github.com/consensys/gnark/logger" "github.com/consensys/gnark-crypto/ecc" @@ -66,7 +67,7 @@ func (cs *system) Solve(witness witness.Witness, opts ...csolver.Option) (any, e // reset the stateful blueprints for i := range cs.Blueprints { - if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful); ok { + if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful[constraint.U64]); ok { b.Reset() } } @@ -135,7 +136,7 @@ func (cs *system) CurveID() ecc.ID { return ecc.BLS24_317 } -func (cs *system) GetCoefficient(i int) (r constraint.Element) { +func (cs *system) GetCoefficient(i int) (r constraint.U64) { copy(r[:], cs.Coefficients[i][:]) return } @@ -288,6 +289,6 @@ func (t *SparseR1CSSolution) ReadFrom(r io.Reader) (int64, error) { return n, err } -func (s *system) AddGkr(gkr constraint.GkrInfo) error { +func (s *system) AddGkr(gkr gkrinfo.StoringInfo) error { return s.System.AddGkr(gkr) } diff --git a/constraint/blueprint.go b/constraint/blueprint.go index 9cc315e5..cfe2fa09 100644 --- a/constraint/blueprint.go +++ b/constraint/blueprint.go @@ -26,24 +26,24 @@ type Blueprint interface { // Solver represents the state of a constraint system solver at runtime. Blueprint can interact // with this object to perform run time logic, solve constraints and assign values in the solution. -type Solver interface { - Field +type Solver[E Element] interface { + Field[E] - GetValue(cID, vID uint32) Element - GetCoeff(cID uint32) Element - SetValue(vID uint32, f Element) + GetValue(cID, vID uint32) E + GetCoeff(cID uint32) E + SetValue(vID uint32, f E) IsSolved(vID uint32) bool // Read interprets input calldata as a LinearExpression, // evaluates it and return the result and the number of uint32 word read. - Read(calldata []uint32) (Element, int) + Read(calldata []uint32) (E, int) } // BlueprintSolvable represents a blueprint that knows how to solve itself. -type BlueprintSolvable interface { +type BlueprintSolvable[E Element] interface { Blueprint // Solve may return an error if the decoded constraint / calldata is unsolvable. - Solve(s Solver, instruction Instruction) error + Solve(s Solver[E], instruction Instruction) error } // BlueprintR1C indicates that the blueprint and associated calldata encodes a R1C @@ -68,8 +68,8 @@ type BlueprintHint interface { } // BlueprintStateful indicates that the blueprint can be reset to its initial state. -type BlueprintStateful interface { - BlueprintSolvable +type BlueprintStateful[E Element] interface { + BlueprintSolvable[E] // Reset is called by the solver between invocation of Solve. Reset() diff --git a/constraint/blueprint_logderivlookup.go b/constraint/blueprint_logderivlookup.go index bb210d3f..82cbacfe 100644 --- a/constraint/blueprint_logderivlookup.go +++ b/constraint/blueprint_logderivlookup.go @@ -10,7 +10,7 @@ import ( // BlueprintLookupHint is a blueprint that facilitates the lookup of values in a table. // It is essentially a hint to the solver, but enables storing the table entries only once. -type BlueprintLookupHint struct { +type BlueprintLookupHint[E Element] struct { EntriesCalldata []uint32 // stores the maxLevel of the entries computed by WireWalker @@ -19,15 +19,16 @@ type BlueprintLookupHint struct { maxLevelOffset int // cache the resolved entries by the solver - cachedEntries []Element + cachedEntries []E cachedOffset int lock sync.Mutex } // ensures BlueprintLookupHint implements the BlueprintStateful interface -var _ BlueprintStateful = (*BlueprintLookupHint)(nil) +var _ BlueprintStateful[U32] = (*BlueprintLookupHint[U32])(nil) +var _ BlueprintStateful[U64] = (*BlueprintLookupHint[U64])(nil) -func (b *BlueprintLookupHint) Solve(s Solver, inst Instruction) error { +func (b *BlueprintLookupHint[E]) Solve(s Solver[E], inst Instruction) error { nbEntries := int(inst.Calldata[1]) // check if we already cached the entries @@ -36,7 +37,8 @@ func (b *BlueprintLookupHint) Solve(s Solver, inst Instruction) error { // we need to cache more entries offset, delta := b.cachedOffset, 0 for i := len(b.cachedEntries); i < nbEntries; i++ { - b.cachedEntries = append(b.cachedEntries, Element{}) + var zero E + b.cachedEntries = append(b.cachedEntries, zero) b.cachedEntries[i], delta = s.Read(b.EntriesCalldata[offset:]) offset += delta } @@ -50,7 +52,7 @@ func (b *BlueprintLookupHint) Solve(s Solver, inst Instruction) error { nbInputs := int(inst.Calldata[2]) // read the inputs from the instruction - inputs := make([]Element, nbInputs) + inputs := make([]E, nbInputs) offset, delta := 3, 0 for i := 0; i < nbInputs; i++ { inputs[i], delta = s.Read(inst.Calldata[offset:]) @@ -71,7 +73,7 @@ func (b *BlueprintLookupHint) Solve(s Solver, inst Instruction) error { return nil } -func (b *BlueprintLookupHint) Reset() { +func (b *BlueprintLookupHint[E]) Reset() { // first we need to compute the capacity; that is 1 element per linear expression in the entries. // this must be accurate since solver is multi threaded and we don't want to resize the slice // while the solver is running. @@ -82,24 +84,24 @@ func (b *BlueprintLookupHint) Reset() { i += 2 * n // skip the linear expression } - b.cachedEntries = make([]Element, 0, capacity) + b.cachedEntries = make([]E, 0, capacity) b.cachedOffset = 0 } -func (b *BlueprintLookupHint) CalldataSize() int { +func (b *BlueprintLookupHint[E]) CalldataSize() int { // variable size return -1 } -func (b *BlueprintLookupHint) NbConstraints() int { +func (b *BlueprintLookupHint[E]) NbConstraints() int { return 0 } // NbOutputs return the number of output wires this blueprint creates. -func (b *BlueprintLookupHint) NbOutputs(inst Instruction) int { +func (b *BlueprintLookupHint[E]) NbOutputs(inst Instruction) int { return int(inst.Calldata[2]) } -func (b *BlueprintLookupHint) UpdateInstructionTree(inst Instruction, tree InstructionTree) Level { +func (b *BlueprintLookupHint[E]) UpdateInstructionTree(inst Instruction, tree InstructionTree) Level { // depend on the table UP to the number of entries at time of instruction creation. nbEntries := int(inst.Calldata[1]) diff --git a/constraint/blueprint_scs.go b/constraint/blueprint_scs.go index 69dad20b..7328cced 100644 --- a/constraint/blueprint_scs.go +++ b/constraint/blueprint_scs.go @@ -14,29 +14,29 @@ var ( // Encodes // // qL⋅xa + qR⋅xb + qO⋅xc + qM⋅(xaxb) + qC == 0 -type BlueprintGenericSparseR1C struct { +type BlueprintGenericSparseR1C[E Element] struct { } -func (b *BlueprintGenericSparseR1C) CalldataSize() int { +func (b *BlueprintGenericSparseR1C[E]) CalldataSize() int { return 9 // number of fields in SparseR1C } -func (b *BlueprintGenericSparseR1C) NbConstraints() int { +func (b *BlueprintGenericSparseR1C[E]) NbConstraints() int { return 1 } -func (b *BlueprintGenericSparseR1C) NbOutputs(inst Instruction) int { +func (b *BlueprintGenericSparseR1C[E]) NbOutputs(inst Instruction) int { return 0 } -func (b *BlueprintGenericSparseR1C) UpdateInstructionTree(inst Instruction, tree InstructionTree) Level { +func (b *BlueprintGenericSparseR1C[E]) UpdateInstructionTree(inst Instruction, tree InstructionTree) Level { return updateInstructionTree(inst.Calldata[0:3], tree) } -func (b *BlueprintGenericSparseR1C) CompressSparseR1C(c *SparseR1C, to *[]uint32) { +func (b *BlueprintGenericSparseR1C[E]) CompressSparseR1C(c *SparseR1C, to *[]uint32) { *to = append(*to, c.XA, c.XB, c.XC, c.QL, c.QR, c.QO, c.QM, c.QC, uint32(c.Commitment)) } -func (b *BlueprintGenericSparseR1C) DecompressSparseR1C(c *SparseR1C, inst Instruction) { +func (b *BlueprintGenericSparseR1C[E]) DecompressSparseR1C(c *SparseR1C, inst Instruction) { c.Clear() c.XA = inst.Calldata[0] @@ -50,7 +50,7 @@ func (b *BlueprintGenericSparseR1C) DecompressSparseR1C(c *SparseR1C, inst Instr c.Commitment = CommitmentConstraint(inst.Calldata[8]) } -func (b *BlueprintGenericSparseR1C) Solve(s Solver, inst Instruction) error { +func (b *BlueprintGenericSparseR1C[E]) Solve(s Solver[E], inst Instruction) error { var c SparseR1C b.DecompressSparseR1C(&c, inst) if c.Commitment != NOT { @@ -126,7 +126,7 @@ func (b *BlueprintGenericSparseR1C) Solve(s Solver, inst Instruction) error { return nil } -func (b *BlueprintGenericSparseR1C) checkConstraint(c *SparseR1C, s Solver) error { +func (b *BlueprintGenericSparseR1C[E]) checkConstraint(c *SparseR1C, s Solver[E]) error { l := s.GetValue(c.QL, c.XA) r := s.GetValue(c.QR, c.XB) m0 := s.GetValue(c.QM, c.XA) @@ -156,27 +156,27 @@ func (b *BlueprintGenericSparseR1C) checkConstraint(c *SparseR1C, s Solver) erro // Encodes // // qM⋅(xaxb) == xc -type BlueprintSparseR1CMul struct{} +type BlueprintSparseR1CMul[E Element] struct{} -func (b *BlueprintSparseR1CMul) CalldataSize() int { +func (b *BlueprintSparseR1CMul[E]) CalldataSize() int { return 4 } -func (b *BlueprintSparseR1CMul) NbConstraints() int { +func (b *BlueprintSparseR1CMul[E]) NbConstraints() int { return 1 } -func (b *BlueprintSparseR1CMul) NbOutputs(inst Instruction) int { +func (b *BlueprintSparseR1CMul[E]) NbOutputs(inst Instruction) int { return 0 } -func (b *BlueprintSparseR1CMul) UpdateInstructionTree(inst Instruction, tree InstructionTree) Level { +func (b *BlueprintSparseR1CMul[E]) UpdateInstructionTree(inst Instruction, tree InstructionTree) Level { return updateInstructionTree(inst.Calldata[0:3], tree) } -func (b *BlueprintSparseR1CMul) CompressSparseR1C(c *SparseR1C, to *[]uint32) { +func (b *BlueprintSparseR1CMul[E]) CompressSparseR1C(c *SparseR1C, to *[]uint32) { *to = append(*to, c.XA, c.XB, c.XC, c.QM) } -func (b *BlueprintSparseR1CMul) Solve(s Solver, inst Instruction) error { +func (b *BlueprintSparseR1CMul[E]) Solve(s Solver[E], inst Instruction) error { // qM⋅(xaxb) == xc m0 := s.GetValue(inst.Calldata[3], inst.Calldata[0]) m1 := s.GetValue(CoeffIdOne, inst.Calldata[1]) @@ -187,7 +187,7 @@ func (b *BlueprintSparseR1CMul) Solve(s Solver, inst Instruction) error { return nil } -func (b *BlueprintSparseR1CMul) DecompressSparseR1C(c *SparseR1C, inst Instruction) { +func (b *BlueprintSparseR1CMul[E]) DecompressSparseR1C(c *SparseR1C, inst Instruction) { c.Clear() c.XA = inst.Calldata[0] c.XB = inst.Calldata[1] @@ -200,27 +200,27 @@ func (b *BlueprintSparseR1CMul) DecompressSparseR1C(c *SparseR1C, inst Instructi // Encodes // // qL⋅xa + qR⋅xb + qC == xc -type BlueprintSparseR1CAdd struct{} +type BlueprintSparseR1CAdd[E Element] struct{} -func (b *BlueprintSparseR1CAdd) CalldataSize() int { +func (b *BlueprintSparseR1CAdd[E]) CalldataSize() int { return 6 } -func (b *BlueprintSparseR1CAdd) NbConstraints() int { +func (b *BlueprintSparseR1CAdd[E]) NbConstraints() int { return 1 } -func (b *BlueprintSparseR1CAdd) NbOutputs(inst Instruction) int { +func (b *BlueprintSparseR1CAdd[E]) NbOutputs(inst Instruction) int { return 0 } -func (b *BlueprintSparseR1CAdd) UpdateInstructionTree(inst Instruction, tree InstructionTree) Level { +func (b *BlueprintSparseR1CAdd[E]) UpdateInstructionTree(inst Instruction, tree InstructionTree) Level { return updateInstructionTree(inst.Calldata[0:3], tree) } -func (b *BlueprintSparseR1CAdd) CompressSparseR1C(c *SparseR1C, to *[]uint32) { +func (b *BlueprintSparseR1CAdd[E]) CompressSparseR1C(c *SparseR1C, to *[]uint32) { *to = append(*to, c.XA, c.XB, c.XC, c.QL, c.QR, c.QC) } -func (blueprint *BlueprintSparseR1CAdd) Solve(s Solver, inst Instruction) error { +func (blueprint *BlueprintSparseR1CAdd[E]) Solve(s Solver[E], inst Instruction) error { // a + b + k == c a := s.GetValue(inst.Calldata[3], inst.Calldata[0]) b := s.GetValue(inst.Calldata[4], inst.Calldata[1]) @@ -233,7 +233,7 @@ func (blueprint *BlueprintSparseR1CAdd) Solve(s Solver, inst Instruction) error return nil } -func (b *BlueprintSparseR1CAdd) DecompressSparseR1C(c *SparseR1C, inst Instruction) { +func (b *BlueprintSparseR1CAdd[E]) DecompressSparseR1C(c *SparseR1C, inst Instruction) { c.Clear() c.XA = inst.Calldata[0] c.XB = inst.Calldata[1] @@ -249,27 +249,27 @@ func (b *BlueprintSparseR1CAdd) DecompressSparseR1C(c *SparseR1C, inst Instructi // // qL⋅xa + qM⋅(xa*xa) == 0 // that is v + -v*v == 0 -type BlueprintSparseR1CBool struct{} +type BlueprintSparseR1CBool[E Element] struct{} -func (b *BlueprintSparseR1CBool) CalldataSize() int { +func (b *BlueprintSparseR1CBool[E]) CalldataSize() int { return 3 } -func (b *BlueprintSparseR1CBool) NbConstraints() int { +func (b *BlueprintSparseR1CBool[E]) NbConstraints() int { return 1 } -func (b *BlueprintSparseR1CBool) NbOutputs(inst Instruction) int { +func (b *BlueprintSparseR1CBool[E]) NbOutputs(inst Instruction) int { return 0 } -func (b *BlueprintSparseR1CBool) UpdateInstructionTree(inst Instruction, tree InstructionTree) Level { +func (b *BlueprintSparseR1CBool[E]) UpdateInstructionTree(inst Instruction, tree InstructionTree) Level { return updateInstructionTree(inst.Calldata[0:1], tree) } -func (b *BlueprintSparseR1CBool) CompressSparseR1C(c *SparseR1C, to *[]uint32) { +func (b *BlueprintSparseR1CBool[E]) CompressSparseR1C(c *SparseR1C, to *[]uint32) { *to = append(*to, c.XA, c.QL, c.QM) } -func (blueprint *BlueprintSparseR1CBool) Solve(s Solver, inst Instruction) error { +func (blueprint *BlueprintSparseR1CBool[E]) Solve(s Solver[E], inst Instruction) error { // all wires are already solved, we just check the constraint. v1 := s.GetValue(inst.Calldata[1], inst.Calldata[0]) v2 := s.GetValue(inst.Calldata[2], inst.Calldata[0]) @@ -282,7 +282,7 @@ func (blueprint *BlueprintSparseR1CBool) Solve(s Solver, inst Instruction) error return nil } -func (b *BlueprintSparseR1CBool) DecompressSparseR1C(c *SparseR1C, inst Instruction) { +func (b *BlueprintSparseR1CBool[E]) DecompressSparseR1C(c *SparseR1C, inst Instruction) { c.Clear() c.XA = inst.Calldata[0] c.XB = c.XA diff --git a/constraint/bn254/coeff.go b/constraint/bn254/coeff.go index ac0b2d11..d8c1b217 100644 --- a/constraint/bn254/coeff.go +++ b/constraint/bn254/coeff.go @@ -8,9 +8,10 @@ package cs import ( "encoding/binary" "errors" + "math/big" + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/utils" - "math/big" "github.com/consensys/gnark-crypto/ecc/bn254/fr" ) @@ -73,7 +74,7 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { return nil } -func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { +func (ct *CoeffTable) AddCoeff(coeff constraint.U64) uint32 { c := (*fr.Element)(coeff[:]) var cID uint32 if c.IsZero() { @@ -99,7 +100,7 @@ func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { return cID } -func (ct *CoeffTable) MakeTerm(coeff constraint.Element, variableID int) constraint.Term { +func (ct *CoeffTable) MakeTerm(coeff constraint.U64, variableID int) constraint.Term { cID := ct.AddCoeff(coeff) return constraint.Term{VID: uint32(variableID), CID: cID} } @@ -112,7 +113,7 @@ func (ct *CoeffTable) CoeffToString(cID int) string { // implements constraint.Field type field struct{} -var _ constraint.Field = &field{} +var _ constraint.Field[constraint.U64] = &field{} var ( two fr.Element @@ -128,7 +129,7 @@ func init() { minusTwo.Neg(&two) } -func (engine *field) FromInterface(i interface{}) constraint.Element { +func (engine *field) FromInterface(i interface{}) constraint.U64 { var e fr.Element if _, err := e.SetInterface(i); err != nil { // need to clean that --> some code path are dissimilar @@ -137,43 +138,43 @@ func (engine *field) FromInterface(i interface{}) constraint.Element { b := utils.FromInterface(i) e.SetBigInt(&b) } - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) ToBigInt(c constraint.Element) *big.Int { +func (engine *field) ToBigInt(c constraint.U64) *big.Int { e := (*fr.Element)(c[:]) r := new(big.Int) e.BigInt(r) return r } -func (engine *field) Mul(a, b constraint.Element) constraint.Element { +func (engine *field) Mul(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Mul(_a, _b) return a } -func (engine *field) Add(a, b constraint.Element) constraint.Element { +func (engine *field) Add(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Add(_a, _b) return a } -func (engine *field) Sub(a, b constraint.Element) constraint.Element { +func (engine *field) Sub(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Sub(_a, _b) return a } -func (engine *field) Neg(a constraint.Element) constraint.Element { +func (engine *field) Neg(a constraint.U64) constraint.U64 { e := (*fr.Element)(a[:]) e.Neg(e) return a } -func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { +func (engine *field) Inverse(a constraint.U64) (constraint.U64, bool) { if a.IsZero() { return a, false } @@ -193,24 +194,24 @@ func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { return a, true } -func (engine *field) IsOne(a constraint.Element) bool { +func (engine *field) IsOne(a constraint.U64) bool { e := (*fr.Element)(a[:]) return e.IsOne() } -func (engine *field) One() constraint.Element { +func (engine *field) One() constraint.U64 { e := fr.One() - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) String(a constraint.Element) string { +func (engine *field) String(a constraint.U64) string { e := (*fr.Element)(a[:]) return e.String() } -func (engine *field) Uint64(a constraint.Element) (uint64, bool) { +func (engine *field) Uint64(a constraint.U64) (uint64, bool) { e := (*fr.Element)(a[:]) if !e.IsUint64() { return 0, false diff --git a/constraint/bn254/gkr.go b/constraint/bn254/gkr.go deleted file mode 100644 index 88dd7905..00000000 --- a/constraint/bn254/gkr.go +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2020-2025 Consensys Software Inc. -// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. - -// Code generated by gnark DO NOT EDIT - -package cs - -import ( - "fmt" - "github.com/consensys/gnark-crypto/ecc/bn254/fr" - "github.com/consensys/gnark-crypto/ecc/bn254/fr/gkr" - "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" - fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" - "github.com/consensys/gnark-crypto/utils" - "github.com/consensys/gnark/constraint" - hint "github.com/consensys/gnark/constraint/solver" - algo_utils "github.com/consensys/gnark/internal/utils" - "hash" - "math/big" - "sync" -) - -type GkrSolvingData struct { - assignments gkr.WireAssignment - circuit gkr.Circuit - memoryPool polynomial.Pool - workers *utils.WorkerPool -} - -func convertCircuit(noPtr constraint.GkrCircuit) (gkr.Circuit, error) { - resCircuit := make(gkr.Circuit, len(noPtr)) - var found bool - for i := range noPtr { - if resCircuit[i].Gate, found = gkr.Gates[noPtr[i].Gate]; !found && noPtr[i].Gate != "" { - return nil, fmt.Errorf("gate \"%s\" not found", noPtr[i].Gate) - } - resCircuit[i].Inputs = algo_utils.Map(noPtr[i].Inputs, algo_utils.SlicePtrAt(resCircuit)) - } - return resCircuit, nil -} - -func (d *GkrSolvingData) init(info constraint.GkrInfo) (assignment gkrAssignment, err error) { - if d.circuit, err = convertCircuit(info.Circuit); err != nil { - return - } - d.memoryPool = polynomial.NewPool(d.circuit.MemoryRequirements(info.NbInstances)...) - d.workers = utils.NewWorkerPool() - - assignment = make(gkrAssignment, len(d.circuit)) - d.assignments = make(gkr.WireAssignment, len(d.circuit)) - for i := range assignment { - assignment[i] = d.memoryPool.Make(info.NbInstances) - d.assignments[&d.circuit[i]] = assignment[i] - } - return -} - -func (d *GkrSolvingData) dumpAssignments() { - for _, p := range d.assignments { - d.memoryPool.Dump(p) - } -} - -// this module assumes that wire and instance indexes respect dependencies - -type gkrAssignment [][]fr.Element //gkrAssignment is indexed wire first, instance second - -func (a gkrAssignment) setOuts(circuit constraint.GkrCircuit, outs []*big.Int) { - outsI := 0 - for i := range circuit { - if circuit[i].IsOutput() { - for j := range a[i] { - a[i][j].BigInt(outs[outsI]) - outsI++ - } - } - } - // Check if outsI == len(outs)? -} - -func GkrSolveHint(info constraint.GkrInfo, solvingData *GkrSolvingData) hint.Hint { - return func(_ *big.Int, ins, outs []*big.Int) error { - // assumes assignmentVector is arranged wire first, instance second in order of solution - circuit := info.Circuit - nbInstances := info.NbInstances - offsets := info.AssignmentOffsets() - assignment, err := solvingData.init(info) - if err != nil { - return err - } - chunks := circuit.Chunks(nbInstances) - - solveTask := func(chunkOffset int) utils.Task { - return func(startInChunk, endInChunk int) { - start := startInChunk + chunkOffset - end := endInChunk + chunkOffset - inputs := solvingData.memoryPool.Make(info.MaxNIns) - dependencyHeads := make([]int, len(circuit)) - for wI, w := range circuit { - dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { - return w.Dependencies[i].InputInstance - }, len(w.Dependencies), start) - } - - for instanceI := start; instanceI < end; instanceI++ { - for wireI, wire := range circuit { - if wire.IsInput() { - if dependencyHeads[wireI] < len(wire.Dependencies) && instanceI == wire.Dependencies[dependencyHeads[wireI]].InputInstance { - dep := wire.Dependencies[dependencyHeads[wireI]] - assignment[wireI][instanceI].Set(&assignment[dep.OutputWire][dep.OutputInstance]) - dependencyHeads[wireI]++ - } else { - assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) - } - } else { - // assemble the inputs - inputIndexes := info.Circuit[wireI].Inputs - for i, inputI := range inputIndexes { - inputs[i].Set(&assignment[inputI][instanceI]) - } - gate := solvingData.circuit[wireI].Gate - assignment[wireI][instanceI] = gate.Evaluate(inputs[:len(inputIndexes)]...) - } - } - } - solvingData.memoryPool.Dump(inputs) - } - } - - start := 0 - for _, end := range chunks { - solvingData.workers.Submit(end-start, solveTask(start), 1024).Wait() - start = end - } - - assignment.setOuts(info.Circuit, outs) - - return nil - } -} - -func frToBigInts(dst []*big.Int, src []fr.Element) { - for i := range src { - src[i].BigInt(dst[i]) - } -} - -func GkrProveHint(hashName string, data *GkrSolvingData) hint.Hint { - - return func(_ *big.Int, ins, outs []*big.Int) error { - insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called - b := make([]byte, fr.Bytes) - i.FillBytes(b) - return b[:] - }) - - hsh, err := GetHashBuilder(hashName) - if err != nil { - return err - } - - proof, err := gkr.Prove(data.circuit, data.assignments, fiatshamir.WithHash(hsh(), insBytes...), gkr.WithPool(&data.memoryPool), gkr.WithWorkers(data.workers)) - if err != nil { - return err - } - - // serialize proof: TODO: In gnark-crypto? - offset := 0 - for i := range proof { - for _, poly := range proof[i].PartialSumPolys { - frToBigInts(outs[offset:], poly) - offset += len(poly) - } - if proof[i].FinalEvalProof != nil { - finalEvalProof := proof[i].FinalEvalProof.([]fr.Element) - frToBigInts(outs[offset:], finalEvalProof) - offset += len(finalEvalProof) - } - } - - data.dumpAssignments() - - return nil - - } -} - -// TODO: Move to gnark-crypto -var ( - hashBuilderRegistry = make(map[string]func() hash.Hash) - hasBuilderLock sync.RWMutex -) - -func RegisterHashBuilder(name string, builder func() hash.Hash) { - hasBuilderLock.Lock() - defer hasBuilderLock.Unlock() - hashBuilderRegistry[name] = builder -} - -func GetHashBuilder(name string) (func() hash.Hash, error) { - hasBuilderLock.RLock() - defer hasBuilderLock.RUnlock() - builder, ok := hashBuilderRegistry[name] - if !ok { - return nil, fmt.Errorf("hash function not found") - } - return builder, nil -} - -// For testing purposes -type ConstPseudoHash int - -func (c ConstPseudoHash) Write(p []byte) (int, error) { - return len(p), nil -} - -func (c ConstPseudoHash) Sum([]byte) []byte { - var x fr.Element - x.SetInt64(int64(c)) - res := x.Bytes() - return res[:] -} - -func (c ConstPseudoHash) Reset() {} - -func (c ConstPseudoHash) Size() int { - return fr.Bytes -} - -func (c ConstPseudoHash) BlockSize() int { - return fr.Bytes -} diff --git a/constraint/bn254/r1cs_test.go b/constraint/bn254/r1cs_test.go index d991531b..c1f47a66 100644 --- a/constraint/bn254/r1cs_test.go +++ b/constraint/bn254/r1cs_test.go @@ -7,12 +7,14 @@ package cs_test import ( "bytes" + "reflect" + "testing" + + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/internal/backend/circuits" - "reflect" - "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -29,8 +31,9 @@ func TestSerialization(t *testing.T) { for name := range circuits.Circuits { t.Run(name, func(t *testing.T) { tc := circuits.Circuits[name] + builder := r1cs.NewBuilder[constraint.U64] - r1cs1, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs1, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -39,7 +42,7 @@ func TestSerialization(t *testing.T) { } // compile a second time to ensure determinism - r1cs2, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs2, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -146,7 +149,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("scs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), scs.NewBuilder, &c) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), scs.NewBuilder, &c) if err != nil { b.Fatal(err) } @@ -160,7 +163,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("r1cs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) if err != nil { b.Fatal(err) } diff --git a/constraint/bn254/solver.go b/constraint/bn254/solver.go index 0903eecc..923bd1a2 100644 --- a/constraint/bn254/solver.go +++ b/constraint/bn254/solver.go @@ -8,18 +8,23 @@ package cs import ( "errors" "fmt" - "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/field/pool" - "github.com/consensys/gnark/constraint" - csolver "github.com/consensys/gnark/constraint/solver" - "github.com/rs/zerolog" "math" "math/big" + "os" "strconv" "strings" "sync" "sync/atomic" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/pool" + "github.com/consensys/gnark/constraint" + csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/constraint/solver/gkrgates" + gkr "github.com/consensys/gnark/internal/gkr/bn254" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/rs/zerolog" + "github.com/consensys/gnark-crypto/ecc/bn254/fr" ) @@ -47,10 +52,14 @@ type solver struct { func newSolver(cs *system, witness fr.Vector, opts ...csolver.Option) (*solver, error) { // add GKR options to overwrite the placeholder if cs.GkrInfo.Is() { - var gkrData GkrSolvingData + var gkrData gkr.SolvingData + solvingInfo, err := gkrtypes.StoringToSolvingInfo(cs.GkrInfo, gkrgates.Get) + if err != nil { + return nil, err + } opts = append(opts, - csolver.OverrideHint(cs.GkrInfo.SolveHintID, GkrSolveHint(cs.GkrInfo, &gkrData)), - csolver.OverrideHint(cs.GkrInfo.ProveHintID, GkrProveHint(cs.GkrInfo.HashName, &gkrData))) + csolver.OverrideHint(cs.GkrInfo.SolveHintID, gkr.SolveHint(solvingInfo, &gkrData)), + csolver.OverrideHint(cs.GkrInfo.ProveHintID, gkr.ProveHint(cs.GkrInfo.HashName, &gkrData))) } // parse options opt, err := csolver.NewConfig(opts...) @@ -332,18 +341,18 @@ func (solver *solver) divByCoeff(res *fr.Element, cID uint32) { } // Implement constraint.Solver -func (s *solver) GetValue(cID, vID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetValue(cID, vID uint32) constraint.U64 { + var r constraint.U64 e := s.computeTerm(constraint.Term{CID: cID, VID: vID}) copy(r[:], e[:]) return r } -func (s *solver) GetCoeff(cID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetCoeff(cID uint32) constraint.U64 { + var r constraint.U64 copy(r[:], s.Coefficients[cID][:]) return r } -func (s *solver) SetValue(vID uint32, f constraint.Element) { +func (s *solver) SetValue(vID uint32, f constraint.U64) { s.set(int(vID), *(*fr.Element)(f[:])) } @@ -353,7 +362,7 @@ func (s *solver) IsSolved(vID uint32) bool { // Read interprets input calldata as either a LinearExpression (if R1CS) or a Term (if Plonkish), // evaluates it and return the result and the number of uint32 word read. -func (s *solver) Read(calldata []uint32) (constraint.Element, int) { +func (s *solver) Read(calldata []uint32) (constraint.U64, int) { if s.Type == constraint.SystemSparseR1CS { if calldata[0] != 1 { panic("invalid calldata") @@ -369,7 +378,7 @@ func (s *solver) Read(calldata []uint32) (constraint.Element, int) { j += 2 } - var ret constraint.Element + var ret constraint.U64 copy(ret[:], r[:]) return ret, j } @@ -393,7 +402,7 @@ func (solver *solver) processInstruction(pi constraint.PackedInstruction, scratc } // blueprint declared "I know how to solve this." - if bc, ok := blueprint.(constraint.BlueprintSolvable); ok { + if bc, ok := blueprint.(constraint.BlueprintSolvable[constraint.U64]); ok { if err := bc.Solve(solver, inst); err != nil { return solver.wrapErrWithDebugInfo(cID, err) } @@ -413,6 +422,9 @@ func (solver *solver) processInstruction(pi constraint.PackedInstruction, scratc // run runs the solver. it return an error if a constraint is not satisfied or if not all wires // were instantiated. func (solver *solver) run() error { + if os.Getenv("DISABLE_GOROUTINE") == "1" { + return solver.serialRun() + } // minWorkPerCPU is the minimum target number of constraint a task should hold // in other words, if a level has less than minWorkPerCPU, it will not be parallelized and executed // sequentially without sync. @@ -520,6 +532,25 @@ func (solver *solver) run() error { return nil } +func (solver *solver) serialRun() error { + //fmt.Printf("solver serialRun\n") + + var scratch scratch + // for each level, we push the tasks + for _, level := range solver.Levels { + // we do it sequentially + for _, i := range level { + if err := solver.processInstruction(solver.Instructions[i], &scratch); err != nil { + return err + } + } + } + if int(solver.nbSolved) != len(solver.values) { + return errors.New("solver didn't assign a value to all wires") + } + return nil +} + // solveR1C compute unsolved wires in the constraint, if any and set the solver accordingly // // returns an error if the solver called a hint function that errored diff --git a/constraint/bn254/system.go b/constraint/bn254/system.go index 50563c00..bc7ebbc2 100644 --- a/constraint/bn254/system.go +++ b/constraint/bn254/system.go @@ -12,6 +12,7 @@ import ( "github.com/consensys/gnark/backend/witness" "github.com/consensys/gnark/constraint" csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/internal/gkr/gkrinfo" "github.com/consensys/gnark/logger" "github.com/consensys/gnark-crypto/ecc" @@ -66,7 +67,7 @@ func (cs *system) Solve(witness witness.Witness, opts ...csolver.Option) (any, e // reset the stateful blueprints for i := range cs.Blueprints { - if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful); ok { + if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful[constraint.U64]); ok { b.Reset() } } @@ -135,7 +136,7 @@ func (cs *system) CurveID() ecc.ID { return ecc.BN254 } -func (cs *system) GetCoefficient(i int) (r constraint.Element) { +func (cs *system) GetCoefficient(i int) (r constraint.U64) { copy(r[:], cs.Coefficients[i][:]) return } @@ -288,6 +289,6 @@ func (t *SparseR1CSSolution) ReadFrom(r io.Reader) (int64, error) { return n, err } -func (s *system) AddGkr(gkr constraint.GkrInfo) error { +func (s *system) AddGkr(gkr gkrinfo.StoringInfo) error { return s.System.AddGkr(gkr) } diff --git a/constraint/bw6-633/coeff.go b/constraint/bw6-633/coeff.go index bd28d0f6..020e16c9 100644 --- a/constraint/bw6-633/coeff.go +++ b/constraint/bw6-633/coeff.go @@ -8,9 +8,10 @@ package cs import ( "encoding/binary" "errors" + "math/big" + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/utils" - "math/big" "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" ) @@ -73,7 +74,7 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { return nil } -func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { +func (ct *CoeffTable) AddCoeff(coeff constraint.U64) uint32 { c := (*fr.Element)(coeff[:]) var cID uint32 if c.IsZero() { @@ -99,7 +100,7 @@ func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { return cID } -func (ct *CoeffTable) MakeTerm(coeff constraint.Element, variableID int) constraint.Term { +func (ct *CoeffTable) MakeTerm(coeff constraint.U64, variableID int) constraint.Term { cID := ct.AddCoeff(coeff) return constraint.Term{VID: uint32(variableID), CID: cID} } @@ -112,7 +113,7 @@ func (ct *CoeffTable) CoeffToString(cID int) string { // implements constraint.Field type field struct{} -var _ constraint.Field = &field{} +var _ constraint.Field[constraint.U64] = &field{} var ( two fr.Element @@ -128,7 +129,7 @@ func init() { minusTwo.Neg(&two) } -func (engine *field) FromInterface(i interface{}) constraint.Element { +func (engine *field) FromInterface(i interface{}) constraint.U64 { var e fr.Element if _, err := e.SetInterface(i); err != nil { // need to clean that --> some code path are dissimilar @@ -137,43 +138,43 @@ func (engine *field) FromInterface(i interface{}) constraint.Element { b := utils.FromInterface(i) e.SetBigInt(&b) } - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) ToBigInt(c constraint.Element) *big.Int { +func (engine *field) ToBigInt(c constraint.U64) *big.Int { e := (*fr.Element)(c[:]) r := new(big.Int) e.BigInt(r) return r } -func (engine *field) Mul(a, b constraint.Element) constraint.Element { +func (engine *field) Mul(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Mul(_a, _b) return a } -func (engine *field) Add(a, b constraint.Element) constraint.Element { +func (engine *field) Add(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Add(_a, _b) return a } -func (engine *field) Sub(a, b constraint.Element) constraint.Element { +func (engine *field) Sub(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Sub(_a, _b) return a } -func (engine *field) Neg(a constraint.Element) constraint.Element { +func (engine *field) Neg(a constraint.U64) constraint.U64 { e := (*fr.Element)(a[:]) e.Neg(e) return a } -func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { +func (engine *field) Inverse(a constraint.U64) (constraint.U64, bool) { if a.IsZero() { return a, false } @@ -193,24 +194,24 @@ func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { return a, true } -func (engine *field) IsOne(a constraint.Element) bool { +func (engine *field) IsOne(a constraint.U64) bool { e := (*fr.Element)(a[:]) return e.IsOne() } -func (engine *field) One() constraint.Element { +func (engine *field) One() constraint.U64 { e := fr.One() - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) String(a constraint.Element) string { +func (engine *field) String(a constraint.U64) string { e := (*fr.Element)(a[:]) return e.String() } -func (engine *field) Uint64(a constraint.Element) (uint64, bool) { +func (engine *field) Uint64(a constraint.U64) (uint64, bool) { e := (*fr.Element)(a[:]) if !e.IsUint64() { return 0, false diff --git a/constraint/bw6-633/gkr.go b/constraint/bw6-633/gkr.go deleted file mode 100644 index 81fbe4c5..00000000 --- a/constraint/bw6-633/gkr.go +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2020-2025 Consensys Software Inc. -// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. - -// Code generated by gnark DO NOT EDIT - -package cs - -import ( - "fmt" - "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" - "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/gkr" - "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/polynomial" - fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" - "github.com/consensys/gnark-crypto/utils" - "github.com/consensys/gnark/constraint" - hint "github.com/consensys/gnark/constraint/solver" - algo_utils "github.com/consensys/gnark/internal/utils" - "hash" - "math/big" - "sync" -) - -type GkrSolvingData struct { - assignments gkr.WireAssignment - circuit gkr.Circuit - memoryPool polynomial.Pool - workers *utils.WorkerPool -} - -func convertCircuit(noPtr constraint.GkrCircuit) (gkr.Circuit, error) { - resCircuit := make(gkr.Circuit, len(noPtr)) - var found bool - for i := range noPtr { - if resCircuit[i].Gate, found = gkr.Gates[noPtr[i].Gate]; !found && noPtr[i].Gate != "" { - return nil, fmt.Errorf("gate \"%s\" not found", noPtr[i].Gate) - } - resCircuit[i].Inputs = algo_utils.Map(noPtr[i].Inputs, algo_utils.SlicePtrAt(resCircuit)) - } - return resCircuit, nil -} - -func (d *GkrSolvingData) init(info constraint.GkrInfo) (assignment gkrAssignment, err error) { - if d.circuit, err = convertCircuit(info.Circuit); err != nil { - return - } - d.memoryPool = polynomial.NewPool(d.circuit.MemoryRequirements(info.NbInstances)...) - d.workers = utils.NewWorkerPool() - - assignment = make(gkrAssignment, len(d.circuit)) - d.assignments = make(gkr.WireAssignment, len(d.circuit)) - for i := range assignment { - assignment[i] = d.memoryPool.Make(info.NbInstances) - d.assignments[&d.circuit[i]] = assignment[i] - } - return -} - -func (d *GkrSolvingData) dumpAssignments() { - for _, p := range d.assignments { - d.memoryPool.Dump(p) - } -} - -// this module assumes that wire and instance indexes respect dependencies - -type gkrAssignment [][]fr.Element //gkrAssignment is indexed wire first, instance second - -func (a gkrAssignment) setOuts(circuit constraint.GkrCircuit, outs []*big.Int) { - outsI := 0 - for i := range circuit { - if circuit[i].IsOutput() { - for j := range a[i] { - a[i][j].BigInt(outs[outsI]) - outsI++ - } - } - } - // Check if outsI == len(outs)? -} - -func GkrSolveHint(info constraint.GkrInfo, solvingData *GkrSolvingData) hint.Hint { - return func(_ *big.Int, ins, outs []*big.Int) error { - // assumes assignmentVector is arranged wire first, instance second in order of solution - circuit := info.Circuit - nbInstances := info.NbInstances - offsets := info.AssignmentOffsets() - assignment, err := solvingData.init(info) - if err != nil { - return err - } - chunks := circuit.Chunks(nbInstances) - - solveTask := func(chunkOffset int) utils.Task { - return func(startInChunk, endInChunk int) { - start := startInChunk + chunkOffset - end := endInChunk + chunkOffset - inputs := solvingData.memoryPool.Make(info.MaxNIns) - dependencyHeads := make([]int, len(circuit)) - for wI, w := range circuit { - dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { - return w.Dependencies[i].InputInstance - }, len(w.Dependencies), start) - } - - for instanceI := start; instanceI < end; instanceI++ { - for wireI, wire := range circuit { - if wire.IsInput() { - if dependencyHeads[wireI] < len(wire.Dependencies) && instanceI == wire.Dependencies[dependencyHeads[wireI]].InputInstance { - dep := wire.Dependencies[dependencyHeads[wireI]] - assignment[wireI][instanceI].Set(&assignment[dep.OutputWire][dep.OutputInstance]) - dependencyHeads[wireI]++ - } else { - assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) - } - } else { - // assemble the inputs - inputIndexes := info.Circuit[wireI].Inputs - for i, inputI := range inputIndexes { - inputs[i].Set(&assignment[inputI][instanceI]) - } - gate := solvingData.circuit[wireI].Gate - assignment[wireI][instanceI] = gate.Evaluate(inputs[:len(inputIndexes)]...) - } - } - } - solvingData.memoryPool.Dump(inputs) - } - } - - start := 0 - for _, end := range chunks { - solvingData.workers.Submit(end-start, solveTask(start), 1024).Wait() - start = end - } - - assignment.setOuts(info.Circuit, outs) - - return nil - } -} - -func frToBigInts(dst []*big.Int, src []fr.Element) { - for i := range src { - src[i].BigInt(dst[i]) - } -} - -func GkrProveHint(hashName string, data *GkrSolvingData) hint.Hint { - - return func(_ *big.Int, ins, outs []*big.Int) error { - insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called - b := make([]byte, fr.Bytes) - i.FillBytes(b) - return b[:] - }) - - hsh, err := GetHashBuilder(hashName) - if err != nil { - return err - } - - proof, err := gkr.Prove(data.circuit, data.assignments, fiatshamir.WithHash(hsh(), insBytes...), gkr.WithPool(&data.memoryPool), gkr.WithWorkers(data.workers)) - if err != nil { - return err - } - - // serialize proof: TODO: In gnark-crypto? - offset := 0 - for i := range proof { - for _, poly := range proof[i].PartialSumPolys { - frToBigInts(outs[offset:], poly) - offset += len(poly) - } - if proof[i].FinalEvalProof != nil { - finalEvalProof := proof[i].FinalEvalProof.([]fr.Element) - frToBigInts(outs[offset:], finalEvalProof) - offset += len(finalEvalProof) - } - } - - data.dumpAssignments() - - return nil - - } -} - -// TODO: Move to gnark-crypto -var ( - hashBuilderRegistry = make(map[string]func() hash.Hash) - hasBuilderLock sync.RWMutex -) - -func RegisterHashBuilder(name string, builder func() hash.Hash) { - hasBuilderLock.Lock() - defer hasBuilderLock.Unlock() - hashBuilderRegistry[name] = builder -} - -func GetHashBuilder(name string) (func() hash.Hash, error) { - hasBuilderLock.RLock() - defer hasBuilderLock.RUnlock() - builder, ok := hashBuilderRegistry[name] - if !ok { - return nil, fmt.Errorf("hash function not found") - } - return builder, nil -} - -// For testing purposes -type ConstPseudoHash int - -func (c ConstPseudoHash) Write(p []byte) (int, error) { - return len(p), nil -} - -func (c ConstPseudoHash) Sum([]byte) []byte { - var x fr.Element - x.SetInt64(int64(c)) - res := x.Bytes() - return res[:] -} - -func (c ConstPseudoHash) Reset() {} - -func (c ConstPseudoHash) Size() int { - return fr.Bytes -} - -func (c ConstPseudoHash) BlockSize() int { - return fr.Bytes -} diff --git a/constraint/bw6-633/r1cs_test.go b/constraint/bw6-633/r1cs_test.go index 9f3c8b59..1cbe0407 100644 --- a/constraint/bw6-633/r1cs_test.go +++ b/constraint/bw6-633/r1cs_test.go @@ -7,12 +7,14 @@ package cs_test import ( "bytes" + "reflect" + "testing" + + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/internal/backend/circuits" - "reflect" - "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -29,8 +31,9 @@ func TestSerialization(t *testing.T) { for name := range circuits.Circuits { t.Run(name, func(t *testing.T) { tc := circuits.Circuits[name] + builder := r1cs.NewBuilder[constraint.U64] - r1cs1, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs1, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -39,7 +42,7 @@ func TestSerialization(t *testing.T) { } // compile a second time to ensure determinism - r1cs2, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs2, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -146,7 +149,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("scs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), scs.NewBuilder, &c) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), scs.NewBuilder, &c) if err != nil { b.Fatal(err) } @@ -160,7 +163,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("r1cs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) if err != nil { b.Fatal(err) } diff --git a/constraint/bw6-633/solver.go b/constraint/bw6-633/solver.go index 89557260..64236979 100644 --- a/constraint/bw6-633/solver.go +++ b/constraint/bw6-633/solver.go @@ -8,11 +8,6 @@ package cs import ( "errors" "fmt" - "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/field/pool" - "github.com/consensys/gnark/constraint" - csolver "github.com/consensys/gnark/constraint/solver" - "github.com/rs/zerolog" "math" "math/big" "strconv" @@ -20,6 +15,15 @@ import ( "sync" "sync/atomic" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/pool" + "github.com/consensys/gnark/constraint" + csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/constraint/solver/gkrgates" + gkr "github.com/consensys/gnark/internal/gkr/bw6-633" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/rs/zerolog" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" ) @@ -47,10 +51,14 @@ type solver struct { func newSolver(cs *system, witness fr.Vector, opts ...csolver.Option) (*solver, error) { // add GKR options to overwrite the placeholder if cs.GkrInfo.Is() { - var gkrData GkrSolvingData + var gkrData gkr.SolvingData + solvingInfo, err := gkrtypes.StoringToSolvingInfo(cs.GkrInfo, gkrgates.Get) + if err != nil { + return nil, err + } opts = append(opts, - csolver.OverrideHint(cs.GkrInfo.SolveHintID, GkrSolveHint(cs.GkrInfo, &gkrData)), - csolver.OverrideHint(cs.GkrInfo.ProveHintID, GkrProveHint(cs.GkrInfo.HashName, &gkrData))) + csolver.OverrideHint(cs.GkrInfo.SolveHintID, gkr.SolveHint(solvingInfo, &gkrData)), + csolver.OverrideHint(cs.GkrInfo.ProveHintID, gkr.ProveHint(cs.GkrInfo.HashName, &gkrData))) } // parse options opt, err := csolver.NewConfig(opts...) @@ -332,18 +340,18 @@ func (solver *solver) divByCoeff(res *fr.Element, cID uint32) { } // Implement constraint.Solver -func (s *solver) GetValue(cID, vID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetValue(cID, vID uint32) constraint.U64 { + var r constraint.U64 e := s.computeTerm(constraint.Term{CID: cID, VID: vID}) copy(r[:], e[:]) return r } -func (s *solver) GetCoeff(cID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetCoeff(cID uint32) constraint.U64 { + var r constraint.U64 copy(r[:], s.Coefficients[cID][:]) return r } -func (s *solver) SetValue(vID uint32, f constraint.Element) { +func (s *solver) SetValue(vID uint32, f constraint.U64) { s.set(int(vID), *(*fr.Element)(f[:])) } @@ -353,7 +361,7 @@ func (s *solver) IsSolved(vID uint32) bool { // Read interprets input calldata as either a LinearExpression (if R1CS) or a Term (if Plonkish), // evaluates it and return the result and the number of uint32 word read. -func (s *solver) Read(calldata []uint32) (constraint.Element, int) { +func (s *solver) Read(calldata []uint32) (constraint.U64, int) { if s.Type == constraint.SystemSparseR1CS { if calldata[0] != 1 { panic("invalid calldata") @@ -369,7 +377,7 @@ func (s *solver) Read(calldata []uint32) (constraint.Element, int) { j += 2 } - var ret constraint.Element + var ret constraint.U64 copy(ret[:], r[:]) return ret, j } @@ -393,7 +401,7 @@ func (solver *solver) processInstruction(pi constraint.PackedInstruction, scratc } // blueprint declared "I know how to solve this." - if bc, ok := blueprint.(constraint.BlueprintSolvable); ok { + if bc, ok := blueprint.(constraint.BlueprintSolvable[constraint.U64]); ok { if err := bc.Solve(solver, inst); err != nil { return solver.wrapErrWithDebugInfo(cID, err) } diff --git a/constraint/bw6-633/system.go b/constraint/bw6-633/system.go index a0dd020f..fb9bffcd 100644 --- a/constraint/bw6-633/system.go +++ b/constraint/bw6-633/system.go @@ -12,6 +12,7 @@ import ( "github.com/consensys/gnark/backend/witness" "github.com/consensys/gnark/constraint" csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/internal/gkr/gkrinfo" "github.com/consensys/gnark/logger" "github.com/consensys/gnark-crypto/ecc" @@ -66,7 +67,7 @@ func (cs *system) Solve(witness witness.Witness, opts ...csolver.Option) (any, e // reset the stateful blueprints for i := range cs.Blueprints { - if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful); ok { + if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful[constraint.U64]); ok { b.Reset() } } @@ -135,7 +136,7 @@ func (cs *system) CurveID() ecc.ID { return ecc.BW6_633 } -func (cs *system) GetCoefficient(i int) (r constraint.Element) { +func (cs *system) GetCoefficient(i int) (r constraint.U64) { copy(r[:], cs.Coefficients[i][:]) return } @@ -288,6 +289,6 @@ func (t *SparseR1CSSolution) ReadFrom(r io.Reader) (int64, error) { return n, err } -func (s *system) AddGkr(gkr constraint.GkrInfo) error { +func (s *system) AddGkr(gkr gkrinfo.StoringInfo) error { return s.System.AddGkr(gkr) } diff --git a/constraint/bw6-761/coeff.go b/constraint/bw6-761/coeff.go index 84d4dee3..5854302e 100644 --- a/constraint/bw6-761/coeff.go +++ b/constraint/bw6-761/coeff.go @@ -8,9 +8,10 @@ package cs import ( "encoding/binary" "errors" + "math/big" + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/utils" - "math/big" "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" ) @@ -73,7 +74,7 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { return nil } -func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { +func (ct *CoeffTable) AddCoeff(coeff constraint.U64) uint32 { c := (*fr.Element)(coeff[:]) var cID uint32 if c.IsZero() { @@ -99,7 +100,7 @@ func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { return cID } -func (ct *CoeffTable) MakeTerm(coeff constraint.Element, variableID int) constraint.Term { +func (ct *CoeffTable) MakeTerm(coeff constraint.U64, variableID int) constraint.Term { cID := ct.AddCoeff(coeff) return constraint.Term{VID: uint32(variableID), CID: cID} } @@ -112,7 +113,7 @@ func (ct *CoeffTable) CoeffToString(cID int) string { // implements constraint.Field type field struct{} -var _ constraint.Field = &field{} +var _ constraint.Field[constraint.U64] = &field{} var ( two fr.Element @@ -128,7 +129,7 @@ func init() { minusTwo.Neg(&two) } -func (engine *field) FromInterface(i interface{}) constraint.Element { +func (engine *field) FromInterface(i interface{}) constraint.U64 { var e fr.Element if _, err := e.SetInterface(i); err != nil { // need to clean that --> some code path are dissimilar @@ -137,43 +138,43 @@ func (engine *field) FromInterface(i interface{}) constraint.Element { b := utils.FromInterface(i) e.SetBigInt(&b) } - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) ToBigInt(c constraint.Element) *big.Int { +func (engine *field) ToBigInt(c constraint.U64) *big.Int { e := (*fr.Element)(c[:]) r := new(big.Int) e.BigInt(r) return r } -func (engine *field) Mul(a, b constraint.Element) constraint.Element { +func (engine *field) Mul(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Mul(_a, _b) return a } -func (engine *field) Add(a, b constraint.Element) constraint.Element { +func (engine *field) Add(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Add(_a, _b) return a } -func (engine *field) Sub(a, b constraint.Element) constraint.Element { +func (engine *field) Sub(a, b constraint.U64) constraint.U64 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Sub(_a, _b) return a } -func (engine *field) Neg(a constraint.Element) constraint.Element { +func (engine *field) Neg(a constraint.U64) constraint.U64 { e := (*fr.Element)(a[:]) e.Neg(e) return a } -func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { +func (engine *field) Inverse(a constraint.U64) (constraint.U64, bool) { if a.IsZero() { return a, false } @@ -193,24 +194,24 @@ func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { return a, true } -func (engine *field) IsOne(a constraint.Element) bool { +func (engine *field) IsOne(a constraint.U64) bool { e := (*fr.Element)(a[:]) return e.IsOne() } -func (engine *field) One() constraint.Element { +func (engine *field) One() constraint.U64 { e := fr.One() - var r constraint.Element + var r constraint.U64 copy(r[:], e[:]) return r } -func (engine *field) String(a constraint.Element) string { +func (engine *field) String(a constraint.U64) string { e := (*fr.Element)(a[:]) return e.String() } -func (engine *field) Uint64(a constraint.Element) (uint64, bool) { +func (engine *field) Uint64(a constraint.U64) (uint64, bool) { e := (*fr.Element)(a[:]) if !e.IsUint64() { return 0, false diff --git a/constraint/bw6-761/gkr.go b/constraint/bw6-761/gkr.go deleted file mode 100644 index 0066d302..00000000 --- a/constraint/bw6-761/gkr.go +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright 2020-2025 Consensys Software Inc. -// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. - -// Code generated by gnark DO NOT EDIT - -package cs - -import ( - "fmt" - "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" - "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/gkr" - "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" - fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" - "github.com/consensys/gnark-crypto/utils" - "github.com/consensys/gnark/constraint" - hint "github.com/consensys/gnark/constraint/solver" - algo_utils "github.com/consensys/gnark/internal/utils" - "hash" - "math/big" - "sync" -) - -type GkrSolvingData struct { - assignments gkr.WireAssignment - circuit gkr.Circuit - memoryPool polynomial.Pool - workers *utils.WorkerPool -} - -func convertCircuit(noPtr constraint.GkrCircuit) (gkr.Circuit, error) { - resCircuit := make(gkr.Circuit, len(noPtr)) - var found bool - for i := range noPtr { - if resCircuit[i].Gate, found = gkr.Gates[noPtr[i].Gate]; !found && noPtr[i].Gate != "" { - return nil, fmt.Errorf("gate \"%s\" not found", noPtr[i].Gate) - } - resCircuit[i].Inputs = algo_utils.Map(noPtr[i].Inputs, algo_utils.SlicePtrAt(resCircuit)) - } - return resCircuit, nil -} - -func (d *GkrSolvingData) init(info constraint.GkrInfo) (assignment gkrAssignment, err error) { - if d.circuit, err = convertCircuit(info.Circuit); err != nil { - return - } - d.memoryPool = polynomial.NewPool(d.circuit.MemoryRequirements(info.NbInstances)...) - d.workers = utils.NewWorkerPool() - - assignment = make(gkrAssignment, len(d.circuit)) - d.assignments = make(gkr.WireAssignment, len(d.circuit)) - for i := range assignment { - assignment[i] = d.memoryPool.Make(info.NbInstances) - d.assignments[&d.circuit[i]] = assignment[i] - } - return -} - -func (d *GkrSolvingData) dumpAssignments() { - for _, p := range d.assignments { - d.memoryPool.Dump(p) - } -} - -// this module assumes that wire and instance indexes respect dependencies - -type gkrAssignment [][]fr.Element //gkrAssignment is indexed wire first, instance second - -func (a gkrAssignment) setOuts(circuit constraint.GkrCircuit, outs []*big.Int) { - outsI := 0 - for i := range circuit { - if circuit[i].IsOutput() { - for j := range a[i] { - a[i][j].BigInt(outs[outsI]) - outsI++ - } - } - } - // Check if outsI == len(outs)? -} - -func GkrSolveHint(info constraint.GkrInfo, solvingData *GkrSolvingData) hint.Hint { - return func(_ *big.Int, ins, outs []*big.Int) error { - // assumes assignmentVector is arranged wire first, instance second in order of solution - circuit := info.Circuit - nbInstances := info.NbInstances - offsets := info.AssignmentOffsets() - assignment, err := solvingData.init(info) - if err != nil { - return err - } - chunks := circuit.Chunks(nbInstances) - - solveTask := func(chunkOffset int) utils.Task { - return func(startInChunk, endInChunk int) { - start := startInChunk + chunkOffset - end := endInChunk + chunkOffset - inputs := solvingData.memoryPool.Make(info.MaxNIns) - dependencyHeads := make([]int, len(circuit)) - for wI, w := range circuit { - dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { - return w.Dependencies[i].InputInstance - }, len(w.Dependencies), start) - } - - for instanceI := start; instanceI < end; instanceI++ { - for wireI, wire := range circuit { - if wire.IsInput() { - if dependencyHeads[wireI] < len(wire.Dependencies) && instanceI == wire.Dependencies[dependencyHeads[wireI]].InputInstance { - dep := wire.Dependencies[dependencyHeads[wireI]] - assignment[wireI][instanceI].Set(&assignment[dep.OutputWire][dep.OutputInstance]) - dependencyHeads[wireI]++ - } else { - assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) - } - } else { - // assemble the inputs - inputIndexes := info.Circuit[wireI].Inputs - for i, inputI := range inputIndexes { - inputs[i].Set(&assignment[inputI][instanceI]) - } - gate := solvingData.circuit[wireI].Gate - assignment[wireI][instanceI] = gate.Evaluate(inputs[:len(inputIndexes)]...) - } - } - } - solvingData.memoryPool.Dump(inputs) - } - } - - start := 0 - for _, end := range chunks { - solvingData.workers.Submit(end-start, solveTask(start), 1024).Wait() - start = end - } - - assignment.setOuts(info.Circuit, outs) - - return nil - } -} - -func frToBigInts(dst []*big.Int, src []fr.Element) { - for i := range src { - src[i].BigInt(dst[i]) - } -} - -func GkrProveHint(hashName string, data *GkrSolvingData) hint.Hint { - - return func(_ *big.Int, ins, outs []*big.Int) error { - insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called - b := make([]byte, fr.Bytes) - i.FillBytes(b) - return b[:] - }) - - hsh, err := GetHashBuilder(hashName) - if err != nil { - return err - } - - proof, err := gkr.Prove(data.circuit, data.assignments, fiatshamir.WithHash(hsh(), insBytes...), gkr.WithPool(&data.memoryPool), gkr.WithWorkers(data.workers)) - if err != nil { - return err - } - - // serialize proof: TODO: In gnark-crypto? - offset := 0 - for i := range proof { - for _, poly := range proof[i].PartialSumPolys { - frToBigInts(outs[offset:], poly) - offset += len(poly) - } - if proof[i].FinalEvalProof != nil { - finalEvalProof := proof[i].FinalEvalProof.([]fr.Element) - frToBigInts(outs[offset:], finalEvalProof) - offset += len(finalEvalProof) - } - } - - data.dumpAssignments() - - return nil - - } -} - -// TODO: Move to gnark-crypto -var ( - hashBuilderRegistry = make(map[string]func() hash.Hash) - hasBuilderLock sync.RWMutex -) - -func RegisterHashBuilder(name string, builder func() hash.Hash) { - hasBuilderLock.Lock() - defer hasBuilderLock.Unlock() - hashBuilderRegistry[name] = builder -} - -func GetHashBuilder(name string) (func() hash.Hash, error) { - hasBuilderLock.RLock() - defer hasBuilderLock.RUnlock() - builder, ok := hashBuilderRegistry[name] - if !ok { - return nil, fmt.Errorf("hash function not found") - } - return builder, nil -} - -// For testing purposes -type ConstPseudoHash int - -func (c ConstPseudoHash) Write(p []byte) (int, error) { - return len(p), nil -} - -func (c ConstPseudoHash) Sum([]byte) []byte { - var x fr.Element - x.SetInt64(int64(c)) - res := x.Bytes() - return res[:] -} - -func (c ConstPseudoHash) Reset() {} - -func (c ConstPseudoHash) Size() int { - return fr.Bytes -} - -func (c ConstPseudoHash) BlockSize() int { - return fr.Bytes -} diff --git a/constraint/bw6-761/r1cs_test.go b/constraint/bw6-761/r1cs_test.go index 4056962b..cd07ebec 100644 --- a/constraint/bw6-761/r1cs_test.go +++ b/constraint/bw6-761/r1cs_test.go @@ -7,12 +7,14 @@ package cs_test import ( "bytes" + "reflect" + "testing" + + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/internal/backend/circuits" - "reflect" - "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -32,8 +34,9 @@ func TestSerialization(t *testing.T) { if testing.Short() && name != "reference_small" { return } + builder := r1cs.NewBuilder[constraint.U64] - r1cs1, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs1, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -42,7 +45,7 @@ func TestSerialization(t *testing.T) { } // compile a second time to ensure determinism - r1cs2, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs2, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -149,7 +152,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("scs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), scs.NewBuilder, &c) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), scs.NewBuilder, &c) if err != nil { b.Fatal(err) } @@ -163,7 +166,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("r1cs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) + ccs, err := frontend.CompileGeneric[constraint.U64](fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) if err != nil { b.Fatal(err) } diff --git a/constraint/bw6-761/solver.go b/constraint/bw6-761/solver.go index e2728751..a65445eb 100644 --- a/constraint/bw6-761/solver.go +++ b/constraint/bw6-761/solver.go @@ -8,11 +8,6 @@ package cs import ( "errors" "fmt" - "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/field/pool" - "github.com/consensys/gnark/constraint" - csolver "github.com/consensys/gnark/constraint/solver" - "github.com/rs/zerolog" "math" "math/big" "strconv" @@ -20,6 +15,15 @@ import ( "sync" "sync/atomic" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/pool" + "github.com/consensys/gnark/constraint" + csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/constraint/solver/gkrgates" + gkr "github.com/consensys/gnark/internal/gkr/bw6-761" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/rs/zerolog" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" ) @@ -47,10 +51,14 @@ type solver struct { func newSolver(cs *system, witness fr.Vector, opts ...csolver.Option) (*solver, error) { // add GKR options to overwrite the placeholder if cs.GkrInfo.Is() { - var gkrData GkrSolvingData + var gkrData gkr.SolvingData + solvingInfo, err := gkrtypes.StoringToSolvingInfo(cs.GkrInfo, gkrgates.Get) + if err != nil { + return nil, err + } opts = append(opts, - csolver.OverrideHint(cs.GkrInfo.SolveHintID, GkrSolveHint(cs.GkrInfo, &gkrData)), - csolver.OverrideHint(cs.GkrInfo.ProveHintID, GkrProveHint(cs.GkrInfo.HashName, &gkrData))) + csolver.OverrideHint(cs.GkrInfo.SolveHintID, gkr.SolveHint(solvingInfo, &gkrData)), + csolver.OverrideHint(cs.GkrInfo.ProveHintID, gkr.ProveHint(cs.GkrInfo.HashName, &gkrData))) } // parse options opt, err := csolver.NewConfig(opts...) @@ -332,18 +340,18 @@ func (solver *solver) divByCoeff(res *fr.Element, cID uint32) { } // Implement constraint.Solver -func (s *solver) GetValue(cID, vID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetValue(cID, vID uint32) constraint.U64 { + var r constraint.U64 e := s.computeTerm(constraint.Term{CID: cID, VID: vID}) copy(r[:], e[:]) return r } -func (s *solver) GetCoeff(cID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetCoeff(cID uint32) constraint.U64 { + var r constraint.U64 copy(r[:], s.Coefficients[cID][:]) return r } -func (s *solver) SetValue(vID uint32, f constraint.Element) { +func (s *solver) SetValue(vID uint32, f constraint.U64) { s.set(int(vID), *(*fr.Element)(f[:])) } @@ -353,7 +361,7 @@ func (s *solver) IsSolved(vID uint32) bool { // Read interprets input calldata as either a LinearExpression (if R1CS) or a Term (if Plonkish), // evaluates it and return the result and the number of uint32 word read. -func (s *solver) Read(calldata []uint32) (constraint.Element, int) { +func (s *solver) Read(calldata []uint32) (constraint.U64, int) { if s.Type == constraint.SystemSparseR1CS { if calldata[0] != 1 { panic("invalid calldata") @@ -369,7 +377,7 @@ func (s *solver) Read(calldata []uint32) (constraint.Element, int) { j += 2 } - var ret constraint.Element + var ret constraint.U64 copy(ret[:], r[:]) return ret, j } @@ -393,7 +401,7 @@ func (solver *solver) processInstruction(pi constraint.PackedInstruction, scratc } // blueprint declared "I know how to solve this." - if bc, ok := blueprint.(constraint.BlueprintSolvable); ok { + if bc, ok := blueprint.(constraint.BlueprintSolvable[constraint.U64]); ok { if err := bc.Solve(solver, inst); err != nil { return solver.wrapErrWithDebugInfo(cID, err) } diff --git a/constraint/bw6-761/system.go b/constraint/bw6-761/system.go index 5816420b..30f4ee71 100644 --- a/constraint/bw6-761/system.go +++ b/constraint/bw6-761/system.go @@ -12,6 +12,7 @@ import ( "github.com/consensys/gnark/backend/witness" "github.com/consensys/gnark/constraint" csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/internal/gkr/gkrinfo" "github.com/consensys/gnark/logger" "github.com/consensys/gnark-crypto/ecc" @@ -66,7 +67,7 @@ func (cs *system) Solve(witness witness.Witness, opts ...csolver.Option) (any, e // reset the stateful blueprints for i := range cs.Blueprints { - if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful); ok { + if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful[constraint.U64]); ok { b.Reset() } } @@ -135,7 +136,7 @@ func (cs *system) CurveID() ecc.ID { return ecc.BW6_761 } -func (cs *system) GetCoefficient(i int) (r constraint.Element) { +func (cs *system) GetCoefficient(i int) (r constraint.U64) { copy(r[:], cs.Coefficients[i][:]) return } @@ -288,6 +289,6 @@ func (t *SparseR1CSSolution) ReadFrom(r io.Reader) (int64, error) { return n, err } -func (s *system) AddGkr(gkr constraint.GkrInfo) error { +func (s *system) AddGkr(gkr gkrinfo.StoringInfo) error { return s.System.AddGkr(gkr) } diff --git a/constraint/core.go b/constraint/core.go index 5d5ff2de..bc04d580 100644 --- a/constraint/core.go +++ b/constraint/core.go @@ -11,7 +11,8 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/debug" - "github.com/consensys/gnark/internal/tinyfield" + "github.com/consensys/gnark/internal/gkr/gkrinfo" + "github.com/consensys/gnark/internal/smallfields" "github.com/consensys/gnark/internal/utils" "github.com/consensys/gnark/logger" "github.com/consensys/gnark/profile" @@ -124,7 +125,7 @@ type System struct { lbWireLevel []Level `cbor:"-"` // at which level we solve a wire. init at -1. CommitmentInfo Commitments - GkrInfo GkrInfo + GkrInfo gkrinfo.StoringInfo genericHint BlueprintID } @@ -202,7 +203,7 @@ func (system *System) CheckSerializationHeader() error { return fmt.Errorf("when parsing serialized modulus: %s", system.ScalarField) } curveID := utils.FieldToCurve(scalarField) - if curveID == ecc.UNKNOWN && scalarField.Cmp(tinyfield.Modulus()) != 0 { + if curveID == ecc.UNKNOWN && !(smallfields.IsSmallField(scalarField)) { return fmt.Errorf("unsupported scalar field %s", scalarField.Text(16)) } system.q = new(big.Int).Set(scalarField) @@ -470,11 +471,11 @@ func putBuffer(buf *[]uint32) { bufPool.Put(buf) } -func (system *System) AddGkr(gkr GkrInfo) error { +func (system *System) AddGkr(gkrInfo gkrinfo.StoringInfo) error { if system.GkrInfo.Is() { return fmt.Errorf("currently only one GKR sub-circuit per SNARK is supported") } - system.GkrInfo = gkr + system.GkrInfo = gkrInfo return nil } diff --git a/constraint/field.go b/constraint/field.go index f63bc910..2e5640fd 100644 --- a/constraint/field.go +++ b/constraint/field.go @@ -2,21 +2,95 @@ package constraint import ( "encoding/binary" + "fmt" "math/big" + + "github.com/consensys/gnark" + "github.com/consensys/gnark/internal/smallfields" ) -// Element represents a term coefficient data. It is instantiated by the concrete -// constraint system implementation. -// Most of the scalar field used in gnark are on 4 uint64, so we have a clear memory overhead here. -type Element [6]uint64 +// U32 represents an element on a single uint32 limb +type U32 [1]uint32 + +// U64 represents an element on 6 uint64 limbs. This fits all scalar fields used +// in gnark-crypto. In concrete implementations, the backends may use less than +// 6 limbs if not necessary. Due to this, there is up to 50% overhead. +type U64 [6]uint64 + +// Element is a generic interface for all elements used in gnark. It is +// implemented by U32 and U64. The interface is used to provide a generic +// interface for all elements used in gnark. +type Element interface { + U32 | U64 + // IsZero returns true if coefficient == 0 + IsZero() bool + // Bytes return the Element as a big-endian byte slice The length of the + // byte slice is 4 for U32 and 48 for U64. The byte slice is in big-endian + // order. + Bytes() []byte +} + +// NewElement creates a new element from a byte slice. The byte slice must be in +// big-endian order. The length of the byte slice is 4 for U32 and 48 for U64. +// The byte slice is copied to the element. The element is returned as the type +// of the element passed as a parameter. The function panics if the byte slice +// is not the correct length or if the element type is not supported. +// +// We use this method instead of having a method on the parametric interface to +// avoid passing the pointer (mutable) parameter. +func NewElement[E Element](b []byte) E { + var e E + switch t := any(&e).(type) { + case *U32: + if len(b) != 4 { + panic(fmt.Sprintf("wrong length, expected 4 got %d", len(b))) + } + t[0] = binary.BigEndian.Uint32(b[0:4]) + case *U64: + if len(b) != 48 { + panic(fmt.Sprintf("wrong length, expected 48 got %d", len(b))) + } + t[0] = binary.BigEndian.Uint64(b[40:48]) + t[1] = binary.BigEndian.Uint64(b[32:40]) + t[2] = binary.BigEndian.Uint64(b[24:32]) + t[3] = binary.BigEndian.Uint64(b[16:24]) + t[4] = binary.BigEndian.Uint64(b[8:16]) + t[5] = binary.BigEndian.Uint64(b[0:8]) + default: + panic(fmt.Sprintf("unsupported type %T", t)) + } + return e +} + +// FitsElement returns true if the element fits in the given modulus. This can +// be used to type-switch in the implementation at runtime. +func FitsElement[E Element](modulus *big.Int) bool { + var e E + switch any(e).(type) { + case U32: + if smallfields.IsSmallField(modulus) { + return true + } + return false + case U64: + for _, c := range gnark.Curves() { + if modulus.Cmp(c.ScalarField()) == 0 { + return true + } + } + return false + default: + panic("unsupported type") + } +} // IsZero returns true if coefficient == 0 -func (z *Element) IsZero() bool { +func (z U64) IsZero() bool { return (z[5] | z[4] | z[3] | z[2] | z[1] | z[0]) == 0 } // Bytes return the Element as a big-endian byte slice -func (z *Element) Bytes() [48]byte { +func (z U64) Bytes() []byte { var b [48]byte binary.BigEndian.PutUint64(b[40:48], z[0]) binary.BigEndian.PutUint64(b[32:40], z[1]) @@ -24,30 +98,32 @@ func (z *Element) Bytes() [48]byte { binary.BigEndian.PutUint64(b[16:24], z[3]) binary.BigEndian.PutUint64(b[8:16], z[4]) binary.BigEndian.PutUint64(b[0:8], z[5]) - return b + return b[:] +} + +// IsZero returns true if coefficient == 0 +func (z U32) IsZero() bool { + return (z[0]) == 0 } -// SetBytes sets the Element from a big-endian byte slice -func (z *Element) SetBytes(b [48]byte) { - z[0] = binary.BigEndian.Uint64(b[40:48]) - z[1] = binary.BigEndian.Uint64(b[32:40]) - z[2] = binary.BigEndian.Uint64(b[24:32]) - z[3] = binary.BigEndian.Uint64(b[16:24]) - z[4] = binary.BigEndian.Uint64(b[8:16]) - z[5] = binary.BigEndian.Uint64(b[0:8]) +// Bytes return the Element as a big-endian byte slice +func (z U32) Bytes() []byte { + var b [4]byte + binary.BigEndian.PutUint32(b[0:4], z[0]) + return b[:] } // Field capability to perform arithmetic on Coeff -type Field interface { - FromInterface(interface{}) Element - ToBigInt(Element) *big.Int - Mul(a, b Element) Element - Add(a, b Element) Element - Sub(a, b Element) Element - Neg(a Element) Element - Inverse(a Element) (Element, bool) - One() Element - IsOne(Element) bool - String(Element) string - Uint64(Element) (uint64, bool) +type Field[E Element] interface { + FromInterface(interface{}) E + ToBigInt(E) *big.Int + Mul(a, b E) E + Add(a, b E) E + Sub(a, b E) E + Neg(a E) E + Inverse(a E) (E, bool) + One() E + IsOne(E) bool + String(E) string + Uint64(E) (uint64, bool) } diff --git a/constraint/field_test.go b/constraint/field_test.go new file mode 100644 index 00000000..a5cd66ba --- /dev/null +++ b/constraint/field_test.go @@ -0,0 +1,167 @@ +package constraint + +import ( + "bytes" + "fmt" + "math/big" + "testing" + + "github.com/consensys/gnark" + fr_bn254 "github.com/consensys/gnark-crypto/ecc/bn254/fr" + fr_bw6761 "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + "github.com/consensys/gnark-crypto/field/babybear" + "github.com/consensys/gnark/internal/smallfields" + "github.com/stretchr/testify/require" +) + +const ( + testCaseRandom = iota + testCaseZero + testCaseOne + testCaseNegOne +) + +func TestNewElementRoundtrip(t *testing.T) { + for _, tc := range []struct { + scenario int + }{ + {testCaseRandom}, + {testCaseZero}, + {testCaseOne}, + {testCaseNegOne}, + } { + t.Run(fmt.Sprintf("case=%d", tc.scenario), func(t *testing.T) { + var r1 fr_bn254.Element // [4]uint64 + var r2 fr_bw6761.Element // [6]uint64 + var r3 babybear.Element // [1]uint32 + + switch tc.scenario { + case testCaseRandom: + r1.SetRandom() + r2.SetRandom() + r3.SetRandom() + case testCaseZero: + r1.SetZero() + r2.SetZero() + r3.SetZero() + case testCaseOne: + r1.SetOne() + r2.SetOne() + r3.SetOne() + case testCaseNegOne: + r1.SetOne() + r1.Neg(&r1) + r2.SetOne() + r2.Neg(&r2) + r3.SetOne() + r3.Neg(&r3) + } + + r1b := r1.Bytes() + r2b := r2.Bytes() + r3b := r3.Bytes() + + r1bp := append(r1b[:], make([]byte, 48-len(r1b))...) + + e1 := NewElement[U64](r1bp[:]) + e2 := NewElement[U64](r2b[:]) + e3 := NewElement[U32](r3b[:]) + + e1b := e1.Bytes() + e2b := e2.Bytes() + e3b := e3.Bytes() + + if len(e1b) != len(r1bp) { + t.Fatalf("expected %d, got %d", len(r1bp), len(e1b)) + } + if len(e1b[:32]) != len(r1b) { + t.Fatalf("expected %d, got %d", len(r1b), len(e1b[:32])) + } + if len(e2b) != len(r2b) { + t.Fatalf("expected %d, got %d", len(r2b), len(e2b)) + } + if len(e3b) != len(r3b) { + t.Fatalf("expected %d, got %d", len(r3b), len(e3b)) + } + + if !bytes.Equal(e1b[:32], r1b[:]) { + t.Fatalf("expected %x, got %x", r1b, e1b) + } + if !bytes.Equal(e1b, r1bp[:]) { + t.Fatalf("expected %x, got %x", r1bp, e1b) + } + if !bytes.Equal(e2b, r2b[:]) { + t.Fatalf("expected %x, got %x", r2b, e2b) + } + if !bytes.Equal(e3b, r3b[:]) { + t.Fatalf("expected %x, got %x", r3b, e3b) + } + }) + } + +} + +func TestFitsElement(t *testing.T) { + type tc struct { + isU32 bool + field *big.Int + expectedFits bool + } + var tcs []tc + for _, c := range gnark.Curves() { + tcs = append(tcs, tc{ + isU32: false, + field: c.ScalarField(), + expectedFits: true}, + tc{ + isU32: true, + field: c.ScalarField(), + expectedFits: false, + }) + } + for _, c := range smallfields.Supported() { + tcs = append(tcs, + tc{ + isU32: true, + field: c, + expectedFits: true}, + tc{ + isU32: false, + field: c, + expectedFits: false, + }) + } + for _, tc := range tcs { + t.Run(fmt.Sprintf("isU32=%v,field=%s", tc.isU32, tc.field), func(t *testing.T) { + var res bool + if tc.isU32 { + res = FitsElement[U32](tc.field) + } else { + res = FitsElement[U64](tc.field) + } + if res != tc.expectedFits { + t.Fatalf("expected %v, got %v", tc.expectedFits, res) + } + }) + } +} + +func TestNewElement(t *testing.T) { + assert := require.New(t) + assert.Panics(func() { + NewElement[U64](nil) + }) + assert.Panics(func() { + NewElement[U32](nil) + }) + for _, l := range []int{0, 1, 2, 3, 5, 6} { + assert.Panics(func() { + NewElement[U32](make([]byte, l)) + }) + } + for _, l := range []int{0, 1, 2, 3, 4, 5, 7, 8, 9} { + assert.Panics(func() { + NewElement[U64](make([]byte, l)) + }) + } +} diff --git a/constraint/gkr.go b/constraint/gkr.go deleted file mode 100644 index f9d8727c..00000000 --- a/constraint/gkr.go +++ /dev/null @@ -1,158 +0,0 @@ -package constraint - -import ( - "fmt" - "sort" - - "github.com/consensys/gnark/constraint/solver" - "github.com/consensys/gnark/internal/utils" -) - -type GkrVariable int // Just an alias to hide implementation details. May be more trouble than worth - -type InputDependency struct { - OutputWire int - OutputInstance int - InputInstance int -} - -type GkrWire struct { - Gate string // TODO: Change to description - Inputs []int - Dependencies []InputDependency // nil for input wires - NbUniqueOutputs int -} - -type GkrCircuit []GkrWire - -type GkrInfo struct { - Circuit GkrCircuit - MaxNIns int - NbInstances int - HashName string - SolveHintID solver.HintID - ProveHintID solver.HintID -} - -type GkrPermutations struct { - SortedInstances []int - SortedWires []int - InstancesPermutation []int - WiresPermutation []int -} - -func (w GkrWire) IsInput() bool { - return len(w.Inputs) == 0 -} - -func (w GkrWire) IsOutput() bool { - return w.NbUniqueOutputs == 0 -} - -// AssignmentOffsets returns the index of the first value assigned to a wire TODO: Explain clearly -func (d *GkrInfo) AssignmentOffsets() []int { - c := d.Circuit - res := make([]int, len(c)+1) - for i := range c { - nbExplicitAssignments := 0 - if c[i].IsInput() { - nbExplicitAssignments = d.NbInstances - len(c[i].Dependencies) - } - res[i+1] = res[i] + nbExplicitAssignments - } - return res -} - -func (d *GkrInfo) NewInputVariable() GkrVariable { - i := len(d.Circuit) - d.Circuit = append(d.Circuit, GkrWire{}) - return GkrVariable(i) -} - -// Compile sorts the circuit wires, their dependencies and the instances -func (d *GkrInfo) Compile(nbInstances int) (GkrPermutations, error) { - - var p GkrPermutations - d.NbInstances = nbInstances - // sort the instances to decide the order in which they are to be solved - instanceDeps := make([][]int, nbInstances) - for i := range d.Circuit { - for _, dep := range d.Circuit[i].Dependencies { - instanceDeps[dep.InputInstance] = append(instanceDeps[dep.InputInstance], dep.OutputInstance) - } - } - - p.SortedInstances, _ = utils.TopologicalSort(instanceDeps) - p.InstancesPermutation = utils.InvertPermutation(p.SortedInstances) - - // this whole circuit sorting is a bit of a charade. if things are built using an api, there's no way it could NOT already be topologically sorted - // worth keeping for future-proofing? - - inputs := utils.Map(d.Circuit, func(w GkrWire) []int { - return w.Inputs - }) - - var uniqueOuts [][]int - p.SortedWires, uniqueOuts = utils.TopologicalSort(inputs) - p.WiresPermutation = utils.InvertPermutation(p.SortedWires) - wirePermutationAt := utils.SliceAt(p.WiresPermutation) - sorted := make([]GkrWire, len(d.Circuit)) // TODO: Directly manipulate d.Circuit instead - for newI, oldI := range p.SortedWires { - oldW := d.Circuit[oldI] - - if !oldW.IsInput() { - d.MaxNIns = max(d.MaxNIns, len(oldW.Inputs)) - } - - for j := range oldW.Dependencies { - dep := &oldW.Dependencies[j] - dep.OutputWire = p.WiresPermutation[dep.OutputWire] - dep.InputInstance = p.InstancesPermutation[dep.InputInstance] - dep.OutputInstance = p.InstancesPermutation[dep.OutputInstance] - } - sort.Slice(oldW.Dependencies, func(i, j int) bool { - return oldW.Dependencies[i].InputInstance < oldW.Dependencies[j].InputInstance - }) - for i := 1; i < len(oldW.Dependencies); i++ { - if oldW.Dependencies[i].InputInstance == oldW.Dependencies[i-1].InputInstance { - return p, fmt.Errorf("an input wire can only have one dependency per instance") - } - } // TODO: Check that dependencies and explicit assignments cover all instances - - sorted[newI] = GkrWire{ - Gate: oldW.Gate, - Inputs: utils.Map(oldW.Inputs, wirePermutationAt), - Dependencies: oldW.Dependencies, - NbUniqueOutputs: len(uniqueOuts[oldI]), - } - } - d.Circuit = sorted - - return p, nil -} - -func (d *GkrInfo) Is() bool { - return d.Circuit != nil -} - -// Chunks returns intervals of instances that are independent of each other and can be solved in parallel -func (c GkrCircuit) Chunks(nbInstances int) []int { - res := make([]int, 0, 1) - lastSeenDependencyI := make([]int, len(c)) - - for start, end := 0, 0; start != nbInstances; start = end { - end = nbInstances - endWireI := -1 - for wI, w := range c { - if wDepI := lastSeenDependencyI[wI]; wDepI < len(w.Dependencies) && w.Dependencies[wDepI].InputInstance < end { - end = w.Dependencies[wDepI].InputInstance - endWireI = wI - } - } - if endWireI != -1 { - lastSeenDependencyI[endWireI]++ - } - res = append(res, end) - } - return res -} diff --git a/constraint/koalabear/coeff.go b/constraint/koalabear/coeff.go new file mode 100644 index 00000000..f1a2560c --- /dev/null +++ b/constraint/koalabear/coeff.go @@ -0,0 +1,220 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package cs + +import ( + "encoding/binary" + "errors" + "math/big" + + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/internal/utils" + + fr "github.com/consensys/gnark-crypto/field/koalabear" +) + +// CoeffTable ensure we store unique coefficients in the constraint system +type CoeffTable struct { + Coefficients []fr.Element + mCoeffs map[fr.Element]uint32 // maps coefficient to coeffID +} + +func newCoeffTable(capacity int) CoeffTable { + r := CoeffTable{ + Coefficients: make([]fr.Element, 5, 5+capacity), + mCoeffs: make(map[fr.Element]uint32, capacity), + } + + r.Coefficients[constraint.CoeffIdZero].SetUint64(0) + r.Coefficients[constraint.CoeffIdOne].SetOne() + r.Coefficients[constraint.CoeffIdTwo].SetUint64(2) + r.Coefficients[constraint.CoeffIdMinusOne].SetInt64(-1) + r.Coefficients[constraint.CoeffIdMinusTwo].SetInt64(-2) + + return r + +} + +func (ct *CoeffTable) toBytes() []byte { + buf := make([]byte, 0, 8+len(ct.Coefficients)*fr.Bytes) + ctLen := uint64(len(ct.Coefficients)) + + buf = binary.LittleEndian.AppendUint64(buf, ctLen) + for _, c := range ct.Coefficients { + for _, w := range c { + buf = binary.LittleEndian.AppendUint32(buf, w) + } + } + + return buf +} + +func (ct *CoeffTable) fromBytes(buf []byte) error { + if len(buf) < 8 { + return errors.New("invalid buffer size") + } + ctLen := binary.LittleEndian.Uint64(buf[:8]) + buf = buf[8:] + + if uint64(len(buf)) < ctLen*fr.Bytes { + return errors.New("invalid buffer size") + } + ct.Coefficients = make([]fr.Element, ctLen) + for i := uint64(0); i < ctLen; i++ { + var c fr.Element + k := int(i) * fr.Bytes + for j := 0; j < fr.Limbs; j++ { + c[j] = binary.LittleEndian.Uint32(buf[k+j*4 : k+(j+1)*4]) + } + ct.Coefficients[i] = c + } + return nil +} + +func (ct *CoeffTable) AddCoeff(coeff constraint.U32) uint32 { + c := (*fr.Element)(coeff[:]) + var cID uint32 + if c.IsZero() { + cID = constraint.CoeffIdZero + } else if c.IsOne() { + cID = constraint.CoeffIdOne + } else if c.Equal(&two) { + cID = constraint.CoeffIdTwo + } else if c.Equal(&minusOne) { + cID = constraint.CoeffIdMinusOne + } else if c.Equal(&minusTwo) { + cID = constraint.CoeffIdMinusTwo + } else { + cc := *c + if id, ok := ct.mCoeffs[cc]; ok { + cID = id + } else { + cID = uint32(len(ct.Coefficients)) + ct.Coefficients = append(ct.Coefficients, cc) + ct.mCoeffs[cc] = cID + } + } + return cID +} + +func (ct *CoeffTable) MakeTerm(coeff constraint.U32, variableID int) constraint.Term { + cID := ct.AddCoeff(coeff) + return constraint.Term{VID: uint32(variableID), CID: cID} +} + +// CoeffToString implements constraint.Resolver +func (ct *CoeffTable) CoeffToString(cID int) string { + return ct.Coefficients[cID].String() +} + +// implements constraint.Field +type field struct{} + +var _ constraint.Field[constraint.U32] = &field{} + +var ( + two fr.Element + minusOne fr.Element + minusTwo fr.Element +) + +func init() { + minusOne.SetOne() + minusOne.Neg(&minusOne) + two.SetOne() + two.Double(&two) + minusTwo.Neg(&two) +} + +func (engine *field) FromInterface(i interface{}) constraint.U32 { + var e fr.Element + if _, err := e.SetInterface(i); err != nil { + // need to clean that --> some code path are dissimilar + // for example setting a fr.Element from an fp.Element + // fails with the above but succeeds through big int... (2-chains) + b := utils.FromInterface(i) + e.SetBigInt(&b) + } + var r constraint.U32 + copy(r[:], e[:]) + return r +} +func (engine *field) ToBigInt(c constraint.U32) *big.Int { + e := (*fr.Element)(c[:]) + r := new(big.Int) + e.BigInt(r) + return r + +} +func (engine *field) Mul(a, b constraint.U32) constraint.U32 { + _a := (*fr.Element)(a[:]) + _b := (*fr.Element)(b[:]) + _a.Mul(_a, _b) + return a +} + +func (engine *field) Add(a, b constraint.U32) constraint.U32 { + _a := (*fr.Element)(a[:]) + _b := (*fr.Element)(b[:]) + _a.Add(_a, _b) + return a +} +func (engine *field) Sub(a, b constraint.U32) constraint.U32 { + _a := (*fr.Element)(a[:]) + _b := (*fr.Element)(b[:]) + _a.Sub(_a, _b) + return a +} +func (engine *field) Neg(a constraint.U32) constraint.U32 { + e := (*fr.Element)(a[:]) + e.Neg(e) + return a + +} +func (engine *field) Inverse(a constraint.U32) (constraint.U32, bool) { + if a.IsZero() { + return a, false + } + e := (*fr.Element)(a[:]) + if e.IsZero() { + return a, false + } else if e.IsOne() { + return a, true + } + var t fr.Element + t.Neg(e) + if t.IsOne() { + return a, true + } + + e.Inverse(e) + return a, true +} + +func (engine *field) IsOne(a constraint.U32) bool { + e := (*fr.Element)(a[:]) + return e.IsOne() +} + +func (engine *field) One() constraint.U32 { + e := fr.One() + var r constraint.U32 + copy(r[:], e[:]) + return r +} + +func (engine *field) String(a constraint.U32) string { + e := (*fr.Element)(a[:]) + return e.String() +} + +func (engine *field) Uint64(a constraint.U32) (uint64, bool) { + e := (*fr.Element)(a[:]) + if !e.IsUint64() { + return 0, false + } + return e.Uint64(), true +} diff --git a/constraint/koalabear/marshal.go b/constraint/koalabear/marshal.go new file mode 100644 index 00000000..e85ecc5d --- /dev/null +++ b/constraint/koalabear/marshal.go @@ -0,0 +1,90 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package cs + +import ( + "encoding/binary" + "fmt" + "io" + + "github.com/blang/semver/v4" +) + +// WriteTo encodes R1CS into provided io.Writer using cbor +func (cs *system) WriteTo(w io.Writer) (int64, error) { + b, err := cs.System.ToBytes() + if err != nil { + return 0, err + } + + c := cs.CoeffTable.toBytes() + + totalLen := uint64(len(b) + len(c)) + gnarkVersion := semver.MustParse(cs.GnarkVersion) + // write totalLen, gnarkVersion.Major, gnarkVersion.Minor, gnarkVersion.Patch using + // binary.LittleEndian + if err := binary.Write(w, binary.LittleEndian, totalLen); err != nil { + return 0, err + } + if err := binary.Write(w, binary.LittleEndian, gnarkVersion.Major); err != nil { + return 0, err + } + if err := binary.Write(w, binary.LittleEndian, gnarkVersion.Minor); err != nil { + return 0, err + } + if err := binary.Write(w, binary.LittleEndian, gnarkVersion.Patch); err != nil { + return 0, err + } + + // write the system + n, err := w.Write(b) + if err != nil { + return int64(n), err + } + + // write the coeff table + m, err := w.Write(c) + return int64(n+m) + 4*8, err +} + +// ReadFrom attempts to decode R1CS from io.Reader using cbor +func (cs *system) ReadFrom(r io.Reader) (int64, error) { + var totalLen uint64 + if err := binary.Read(r, binary.LittleEndian, &totalLen); err != nil { + return 0, err + } + + var major, minor, patch uint64 + if err := binary.Read(r, binary.LittleEndian, &major); err != nil { + return 0, err + } + if err := binary.Read(r, binary.LittleEndian, &minor); err != nil { + return 0, err + } + if err := binary.Read(r, binary.LittleEndian, &patch); err != nil { + return 0, err + } + // TODO @gbotrel validate version, duplicate logic with core.go CheckSerializationHeader + if major != 0 || minor < 10 { + return 0, fmt.Errorf("unsupported gnark version %d.%d.%d", major, minor, patch) + } + + data := make([]byte, totalLen) + if _, err := io.ReadFull(r, data); err != nil { + return 0, err + } + n, err := cs.System.FromBytes(data) + if err != nil { + return 0, err + } + data = data[n:] + + if err := cs.CoeffTable.fromBytes(data); err != nil { + return 0, err + } + + return int64(totalLen) + 4*8, nil +} diff --git a/constraint/koalabear/r1cs_test.go b/constraint/koalabear/r1cs_test.go new file mode 100644 index 00000000..8140af4f --- /dev/null +++ b/constraint/koalabear/r1cs_test.go @@ -0,0 +1,183 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package cs_test + +import ( + "bytes" + "reflect" + "testing" + + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/frontend/cs/scs" + "github.com/consensys/gnark/internal/backend/circuits" + "github.com/consensys/gnark/internal/widecommitter" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + + cs "github.com/consensys/gnark/constraint/koalabear" + + fr "github.com/consensys/gnark-crypto/field/koalabear" +) + +func TestSerialization(t *testing.T) { + + var buffer, buffer2 bytes.Buffer + + for name := range circuits.Circuits { + t.Run(name, func(t *testing.T) { + tc := circuits.Circuits[name] + builder := r1cs.NewBuilder[constraint.U32] + if name == "commit" { + // smallfield builders do not support commitment. We use the wrapper which has the methods + builder = widecommitter.From(builder) + } + + r1cs1, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) + if err != nil { + t.Fatal(err) + } + if testing.Short() && r1cs1.GetNbConstraints() > 50 { + return + } + + // compile a second time to ensure determinism + r1cs2, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) + if err != nil { + t.Fatal(err) + } + + { + buffer.Reset() + t.Log(name) + var err error + var written, read int64 + written, err = r1cs1.WriteTo(&buffer) + if err != nil { + t.Fatal(err) + } + var reconstructed cs.R1CS + read, err = reconstructed.ReadFrom(&buffer) + if err != nil { + t.Fatal(err) + } + if written != read { + t.Fatal("didn't read same number of bytes we wrote") + } + + // compare original and reconstructed + if diff := cmp.Diff(r1cs1, &reconstructed, + cmpopts.IgnoreFields(cs.R1CS{}, + "System.q", + "field", + "CoeffTable.mCoeffs", + "System.lbWireLevel", + "System.genericHint", + "System.SymbolTable", + "System.bitLen")); diff != "" { + t.Fatalf("round trip mismatch (-want +got):\n%s", diff) + } + } + + // ensure determinism in compilation / serialization / reconstruction + { + buffer.Reset() + n, err := r1cs1.WriteTo(&buffer) + if err != nil { + t.Fatal(err) + } + if n == 0 { + t.Fatal("No bytes are written") + } + + buffer2.Reset() + _, err = r1cs2.WriteTo(&buffer2) + if err != nil { + t.Fatal(err) + } + + if !bytes.Equal(buffer.Bytes(), buffer2.Bytes()) { + t.Fatal("compilation of R1CS is not deterministic") + } + + var r, r2 cs.R1CS + n, err = r.ReadFrom(&buffer) + if err != nil { + t.Fatal(nil) + } + if n == 0 { + t.Fatal("No bytes are read") + } + _, err = r2.ReadFrom(&buffer2) + if err != nil { + t.Fatal(nil) + } + + if !reflect.DeepEqual(r, r2) { + t.Fatal("compilation of R1CS is not deterministic (reconstruction)") + } + } + }) + + } +} + +const n = 10000 + +type circuit struct { + X frontend.Variable + Y frontend.Variable `gnark:",public"` +} + +func (circuit *circuit) Define(api frontend.API) error { + for i := 0; i < n; i++ { + circuit.X = api.Add(api.Mul(circuit.X, circuit.X), circuit.X, 42) + } + api.AssertIsEqual(circuit.X, circuit.Y) + return nil +} + +func BenchmarkSolve(b *testing.B) { + + var w circuit + w.X = 1 + w.Y = 1 + witness, err := frontend.NewWitness(&w, fr.Modulus()) + if err != nil { + b.Fatal(err) + } + + b.Run("scs", func(b *testing.B) { + var c circuit + ccs, err := frontend.CompileGeneric[constraint.U32](fr.Modulus(), scs.NewBuilder, &c) + if err != nil { + b.Fatal(err) + } + b.Log("scs nbConstraints", ccs.GetNbConstraints()) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = ccs.IsSolved(witness) + } + }) + + b.Run("r1cs", func(b *testing.B) { + var c circuit + ccs, err := frontend.CompileGeneric[constraint.U32](fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) + if err != nil { + b.Fatal(err) + } + b.Log("r1cs nbConstraints", ccs.GetNbConstraints()) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = ccs.IsSolved(witness) + } + }) + +} diff --git a/constraint/koalabear/solver.go b/constraint/koalabear/solver.go new file mode 100644 index 00000000..7e227b1f --- /dev/null +++ b/constraint/koalabear/solver.go @@ -0,0 +1,638 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package cs + +import ( + "errors" + "fmt" + "math" + "math/big" + "strconv" + "strings" + "sync" + "sync/atomic" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/pool" + "github.com/consensys/gnark/constraint" + csolver "github.com/consensys/gnark/constraint/solver" + "github.com/rs/zerolog" + + fr "github.com/consensys/gnark-crypto/field/koalabear" +) + +// solver represent the state of the solver during a call to System.Solve(...) +type solver struct { + *system + + // values and solved are index by the wire (variable) id + values []fr.Element + solved []bool + nbSolved uint64 + + // maps hintID to hint function + mHintsFunctions map[csolver.HintID]csolver.Hint + + // used to out api.Println + logger zerolog.Logger + nbTasks int + + a, b, c fr.Vector // R1CS solver will compute the a,b,c matrices + + q *big.Int +} + +func newSolver(cs *system, witness fr.Vector, opts ...csolver.Option) (*solver, error) { + // parse options + opt, err := csolver.NewConfig(opts...) + if err != nil { + return nil, err + } + + // check witness size + witnessOffset := 0 + if cs.Type == constraint.SystemR1CS { + witnessOffset++ + } + + nbWires := len(cs.Public) + len(cs.Secret) + cs.NbInternalVariables + expectedWitnessSize := len(cs.Public) - witnessOffset + len(cs.Secret) + + if len(witness) != expectedWitnessSize { + return nil, fmt.Errorf("invalid witness size, got %d, expected %d", len(witness), expectedWitnessSize) + } + + // check all hints are there + hintFunctions := opt.HintFunctions + + // hintsDependencies is from compile time; it contains the list of hints the solver **needs** + var missing []string + for hintUUID, hintID := range cs.MHintsDependencies { + if _, ok := hintFunctions[hintUUID]; !ok { + missing = append(missing, hintID) + } + } + + if len(missing) > 0 { + return nil, fmt.Errorf("solver missing hint(s): %v", missing) + } + + s := solver{ + system: cs, + values: make([]fr.Element, nbWires), + solved: make([]bool, nbWires), + mHintsFunctions: hintFunctions, + logger: opt.Logger, + nbTasks: opt.NbTasks, + q: cs.Field(), + } + + // set the witness indexes as solved + if witnessOffset == 1 { + s.solved[0] = true // ONE_WIRE + s.values[0].SetOne() + } + copy(s.values[witnessOffset:], witness) + for i := range witness { + s.solved[i+witnessOffset] = true + } + + // keep track of the number of wire instantiations we do, for a post solve sanity check + // to ensure we instantiated all wires + s.nbSolved += uint64(len(witness) + witnessOffset) + + if s.Type == constraint.SystemR1CS { + n := ecc.NextPowerOfTwo(uint64(cs.GetNbConstraints())) + s.a = make(fr.Vector, cs.GetNbConstraints(), n) + s.b = make(fr.Vector, cs.GetNbConstraints(), n) + s.c = make(fr.Vector, cs.GetNbConstraints(), n) + } + + return &s, nil +} + +func (s *solver) set(id int, value fr.Element) { + if s.solved[id] { + panic("solving the same wire twice should never happen.") + } + s.values[id] = value + s.solved[id] = true + atomic.AddUint64(&s.nbSolved, 1) +} + +// computeTerm computes coeff*variable +func (s *solver) computeTerm(t constraint.Term) fr.Element { + cID, vID := t.CoeffID(), t.WireID() + + if t.IsConstant() { + return s.Coefficients[cID] + } + + if cID != 0 && !s.solved[vID] { + panic("computing a term with an unsolved wire") + } + + switch cID { + case constraint.CoeffIdZero: + return fr.Element{} + case constraint.CoeffIdOne: + return s.values[vID] + case constraint.CoeffIdTwo: + var res fr.Element + res.Double(&s.values[vID]) + return res + case constraint.CoeffIdMinusOne: + var res fr.Element + res.Neg(&s.values[vID]) + return res + default: + var res fr.Element + res.Mul(&s.Coefficients[cID], &s.values[vID]) + return res + } +} + +// r += (t.coeff*t.value) +// TODO @gbotrel check t.IsConstant on the caller side when necessary +func (s *solver) accumulateInto(t constraint.Term, r *fr.Element) { + cID := t.CoeffID() + vID := t.WireID() + + if t.IsConstant() { + r.Add(r, &s.Coefficients[cID]) + return + } + + switch cID { + case constraint.CoeffIdZero: + return + case constraint.CoeffIdOne: + r.Add(r, &s.values[vID]) + case constraint.CoeffIdTwo: + var res fr.Element + res.Double(&s.values[vID]) + r.Add(r, &res) + case constraint.CoeffIdMinusOne: + r.Sub(r, &s.values[vID]) + default: + var res fr.Element + res.Mul(&s.Coefficients[cID], &s.values[vID]) + r.Add(r, &res) + } +} + +// solveWithHint executes a hint and assign the result to its defined outputs. +func (s *solver) solveWithHint(h *constraint.HintMapping) error { + // ensure hint function was provided + f, ok := s.mHintsFunctions[h.HintID] + if !ok { + return errors.New("missing hint function") + } + + // tmp IO big int memory + nbInputs := len(h.Inputs) + nbOutputs := int(h.OutputRange.End - h.OutputRange.Start) + inputs := make([]*big.Int, nbInputs) + outputs := make([]*big.Int, nbOutputs) + for i := 0; i < nbOutputs; i++ { + outputs[i] = pool.BigInt.Get() + outputs[i].SetUint64(0) + } + + q := pool.BigInt.Get() + q.Set(s.q) + + for i := 0; i < nbInputs; i++ { + var v fr.Element + for _, term := range h.Inputs[i] { + if term.IsConstant() { + v.Add(&v, &s.Coefficients[term.CoeffID()]) + continue + } + s.accumulateInto(term, &v) + } + inputs[i] = pool.BigInt.Get() + v.BigInt(inputs[i]) + } + + err := f(q, inputs, outputs) + + var v fr.Element + for i := range outputs { + v.SetBigInt(outputs[i]) + s.set(int(h.OutputRange.Start)+i, v) + pool.BigInt.Put(outputs[i]) + } + + for i := range inputs { + pool.BigInt.Put(inputs[i]) + } + + pool.BigInt.Put(q) + + return err +} + +func (s *solver) printLogs(logs []constraint.LogEntry) { + if s.logger.GetLevel() == zerolog.Disabled { + return + } + + for i := 0; i < len(logs); i++ { + logLine := s.logValue(logs[i]) + s.logger.Debug().Str(zerolog.CallerFieldName, logs[i].Caller).Msg(logLine) + } +} + +const unsolvedVariable = "" + +func (s *solver) logValue(log constraint.LogEntry) string { + var toResolve []interface{} + var ( + eval fr.Element + missingValue bool + ) + for j := 0; j < len(log.ToResolve); j++ { + // before eval le + + missingValue = false + eval.SetZero() + + for _, t := range log.ToResolve[j] { + // for each term in the linear expression + + cID, vID := t.CoeffID(), t.WireID() + if t.IsConstant() { + // just add the constant + eval.Add(&eval, &s.Coefficients[cID]) + continue + } + + if !s.solved[vID] { + missingValue = true + break // stop the loop we can't evaluate. + } + + tv := s.computeTerm(t) + eval.Add(&eval, &tv) + } + + // after + if missingValue { + toResolve = append(toResolve, unsolvedVariable) + } else { + // we have to append our accumulator + toResolve = append(toResolve, eval.String()) + } + + } + if len(log.Stack) > 0 { + var sbb strings.Builder + for _, lID := range log.Stack { + location := s.SymbolTable.Locations[lID] + function := s.SymbolTable.Functions[location.FunctionID] + + sbb.WriteString(function.Name) + sbb.WriteByte('\n') + sbb.WriteByte('\t') + sbb.WriteString(function.Filename) + sbb.WriteByte(':') + sbb.WriteString(strconv.Itoa(int(location.Line))) + sbb.WriteByte('\n') + } + toResolve = append(toResolve, sbb.String()) + } + return fmt.Sprintf(log.Format, toResolve...) +} + +// divByCoeff sets res = res / t.Coeff +func (solver *solver) divByCoeff(res *fr.Element, cID uint32) { + switch cID { + case constraint.CoeffIdOne: + return + case constraint.CoeffIdMinusOne: + res.Neg(res) + case constraint.CoeffIdZero: + panic("division by 0") + default: + // this is slow, but shouldn't happen as divByCoeff is called to + // remove the coeff of an unsolved wire + // but unsolved wires are (in gnark frontend) systematically set with a coeff == 1 or -1 + res.Div(res, &solver.Coefficients[cID]) + } +} + +// Implement constraint.Solver +func (s *solver) GetValue(cID, vID uint32) constraint.U32 { + var r constraint.U32 + e := s.computeTerm(constraint.Term{CID: cID, VID: vID}) + copy(r[:], e[:]) + return r +} +func (s *solver) GetCoeff(cID uint32) constraint.U32 { + var r constraint.U32 + copy(r[:], s.Coefficients[cID][:]) + return r +} +func (s *solver) SetValue(vID uint32, f constraint.U32) { + s.set(int(vID), *(*fr.Element)(f[:])) +} + +func (s *solver) IsSolved(vID uint32) bool { + return s.solved[vID] +} + +// Read interprets input calldata as either a LinearExpression (if R1CS) or a Term (if Plonkish), +// evaluates it and return the result and the number of uint32 word read. +func (s *solver) Read(calldata []uint32) (constraint.U32, int) { + if s.Type == constraint.SystemSparseR1CS { + if calldata[0] != 1 { + panic("invalid calldata") + } + return s.GetValue(calldata[1], calldata[2]), 3 + } + var r fr.Element + n := int(calldata[0]) + j := 1 + for k := 0; k < n; k++ { + // we read k Terms + s.accumulateInto(constraint.Term{CID: calldata[j], VID: calldata[j+1]}, &r) + j += 2 + } + + var ret constraint.U32 + copy(ret[:], r[:]) + return ret, j +} + +// processInstruction decodes the instruction and execute blueprint-defined logic. +// an instruction can encode a hint, a custom constraint or a generic constraint. +func (solver *solver) processInstruction(pi constraint.PackedInstruction, scratch *scratch) error { + // fetch the blueprint + blueprint := solver.Blueprints[pi.BlueprintID] + inst := pi.Unpack(&solver.System) + cID := inst.ConstraintOffset // here we have 1 constraint in the instruction only + + if solver.Type == constraint.SystemR1CS { + if bc, ok := blueprint.(constraint.BlueprintR1C); ok { + // TODO @gbotrel we use the solveR1C method for now, having user-defined + // blueprint for R1CS would require constraint.Solver interface to add methods + // to set a,b,c since it's more efficient to compute these while we solve. + bc.DecompressR1C(&scratch.tR1C, inst) + return solver.solveR1C(cID, &scratch.tR1C) + } + } + + // blueprint declared "I know how to solve this." + if bc, ok := blueprint.(constraint.BlueprintSolvable[constraint.U32]); ok { + if err := bc.Solve(solver, inst); err != nil { + return solver.wrapErrWithDebugInfo(cID, err) + } + return nil + } + + // blueprint encodes a hint, we execute. + // TODO @gbotrel may be worth it to move hint logic in blueprint "solve" + if bc, ok := blueprint.(constraint.BlueprintHint); ok { + bc.DecompressHint(&scratch.tHint, inst) + return solver.solveWithHint(&scratch.tHint) + } + + return nil +} + +// run runs the solver. it return an error if a constraint is not satisfied or if not all wires +// were instantiated. +func (solver *solver) run() error { + // minWorkPerCPU is the minimum target number of constraint a task should hold + // in other words, if a level has less than minWorkPerCPU, it will not be parallelized and executed + // sequentially without sync. + const minWorkPerCPU = 50.0 // TODO @gbotrel revisit that with blocks. + + // cs.Levels has a list of levels, where all constraints in a level l(n) are independent + // and may only have dependencies on previous levels + // for each constraint + // we are guaranteed that each R1C contains at most one unsolved wire + // first we solve the unsolved wire (if any) + // then we check that the constraint is valid + // if a[i] * b[i] != c[i]; it means the constraint is not satisfied + var wg sync.WaitGroup + chTasks := make(chan []uint32, solver.nbTasks) + chError := make(chan error, solver.nbTasks) + + // start a worker pool + // each worker wait on chTasks + // a task is a slice of constraint indexes to be solved + for i := 0; i < solver.nbTasks; i++ { + go func() { + var scratch scratch + for t := range chTasks { + for _, i := range t { + if err := solver.processInstruction(solver.Instructions[i], &scratch); err != nil { + chError <- err + wg.Done() + return + } + } + wg.Done() + } + }() + } + + // clean up pool go routines + defer func() { + close(chTasks) + close(chError) + }() + + var scratch scratch + + // for each level, we push the tasks + for _, level := range solver.Levels { + + // max CPU to use + maxCPU := float64(len(level)) / minWorkPerCPU + + if maxCPU <= 1.0 || solver.nbTasks == 1 { + // we do it sequentially + for _, i := range level { + if err := solver.processInstruction(solver.Instructions[i], &scratch); err != nil { + return err + } + } + continue + } + + // number of tasks for this level is set to number of CPU + // but if we don't have enough work for all our CPU, it can be lower. + nbTasks := solver.nbTasks + maxTasks := int(math.Ceil(maxCPU)) + if nbTasks > maxTasks { + nbTasks = maxTasks + } + nbIterationsPerCpus := len(level) / nbTasks + + // more CPUs than tasks: a CPU will work on exactly one iteration + // note: this depends on minWorkPerCPU constant + if nbIterationsPerCpus < 1 { + nbIterationsPerCpus = 1 + nbTasks = len(level) + } + + extraTasks := len(level) - (nbTasks * nbIterationsPerCpus) + extraTasksOffset := 0 + + for i := 0; i < nbTasks; i++ { + wg.Add(1) + _start := i*nbIterationsPerCpus + extraTasksOffset + _end := _start + nbIterationsPerCpus + if extraTasks > 0 { + _end++ + extraTasks-- + extraTasksOffset++ + } + // since we're never pushing more than num CPU tasks + // we will never be blocked here + chTasks <- level[_start:_end] + } + + // wait for the level to be done + wg.Wait() + + if len(chError) > 0 { + return <-chError + } + } + + if int(solver.nbSolved) != len(solver.values) { + return errors.New("solver didn't assign a value to all wires") + } + + return nil +} + +// solveR1C compute unsolved wires in the constraint, if any and set the solver accordingly +// +// returns an error if the solver called a hint function that errored +// returns false, nil if there was no wire to solve +// returns true, nil if exactly one wire was solved. In that case, it is redundant to check that +// the constraint is satisfied later. +func (solver *solver) solveR1C(cID uint32, r *constraint.R1C) error { + a, b, c := &solver.a[cID], &solver.b[cID], &solver.c[cID] + + // the index of the non-zero entry shows if L, R or O has an uninstantiated wire + // the content is the ID of the wire non instantiated + var loc uint8 + + var termToCompute constraint.Term + + processLExp := func(l constraint.LinearExpression, val *fr.Element, locValue uint8) { + for _, t := range l { + vID := t.WireID() + + // wire is already computed, we just accumulate in val + if solver.solved[vID] { + solver.accumulateInto(t, val) + continue + } + + if loc != 0 { + panic("found more than one wire to instantiate") + } + termToCompute = t + loc = locValue + } + } + + processLExp(r.L, a, 1) + processLExp(r.R, b, 2) + processLExp(r.O, c, 3) + + if loc == 0 { + // there is nothing to solve, may happen if we have an assertion + // (ie a constraints that doesn't yield any output) + // or if we solved the unsolved wires with hint functions + var check fr.Element + if !check.Mul(a, b).Equal(c) { + return solver.wrapErrWithDebugInfo(cID, fmt.Errorf("%s ⋅ %s != %s", a.String(), b.String(), c.String())) + } + return nil + } + + // we compute the wire value and instantiate it + wID := termToCompute.WireID() + + // solver result + var wire fr.Element + + switch loc { + case 1: + if !b.IsZero() { + wire.Div(c, b). + Sub(&wire, a) + a.Add(a, &wire) + } else { + // we didn't actually ensure that a * b == c + var check fr.Element + if !check.Mul(a, b).Equal(c) { + return solver.wrapErrWithDebugInfo(cID, fmt.Errorf("%s ⋅ %s != %s", a.String(), b.String(), c.String())) + } + } + case 2: + if !a.IsZero() { + wire.Div(c, a). + Sub(&wire, b) + b.Add(b, &wire) + } else { + var check fr.Element + if !check.Mul(a, b).Equal(c) { + return solver.wrapErrWithDebugInfo(cID, fmt.Errorf("%s ⋅ %s != %s", a.String(), b.String(), c.String())) + } + } + case 3: + wire.Mul(a, b). + Sub(&wire, c) + + c.Add(c, &wire) + } + + // wire is the term (coeff * value) + // but in the solver we want to store the value only + // note that in gnark frontend, coeff here is always 1 or -1 + solver.divByCoeff(&wire, termToCompute.CID) + solver.set(wID, wire) + + return nil +} + +// UnsatisfiedConstraintError wraps an error with useful metadata on the unsatisfied constraint +type UnsatisfiedConstraintError struct { + Err error + CID int // constraint ID + DebugInfo *string // optional debug info +} + +func (r *UnsatisfiedConstraintError) Error() string { + if r.DebugInfo != nil { + return fmt.Sprintf("constraint #%d is not satisfied: %s", r.CID, *r.DebugInfo) + } + return fmt.Sprintf("constraint #%d is not satisfied: %s", r.CID, r.Err.Error()) +} + +func (solver *solver) wrapErrWithDebugInfo(cID uint32, err error) *UnsatisfiedConstraintError { + var debugInfo *string + if dID, ok := solver.MDebug[int(cID)]; ok { + debugInfo = new(string) + *debugInfo = solver.logValue(solver.DebugInfo[dID]) + } + return &UnsatisfiedConstraintError{CID: int(cID), Err: err, DebugInfo: debugInfo} +} + +// temporary variables to avoid memallocs in hotloop +type scratch struct { + tR1C constraint.R1C + tHint constraint.HintMapping +} diff --git a/constraint/koalabear/system.go b/constraint/koalabear/system.go new file mode 100644 index 00000000..90cce6d1 --- /dev/null +++ b/constraint/koalabear/system.go @@ -0,0 +1,294 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package cs + +import ( + "io" + "time" + + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" + csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/internal/gkr/gkrinfo" + "github.com/consensys/gnark/logger" + + "github.com/consensys/gnark-crypto/ecc" + + fr "github.com/consensys/gnark-crypto/field/koalabear" +) + +type R1CS = system +type SparseR1CS = system + +// system is a curved-typed constraint.System with a concrete coefficient table (fr.Element) +type system struct { + constraint.System + CoeffTable + field +} + +// NewR1CS is a constructor for R1CS. It is meant to be use by gnark frontend only, +// and should not be used by gnark users. See groth16.NewCS(...) instead. +func NewR1CS(capacity int) *R1CS { + return newSystem(capacity, constraint.SystemR1CS) +} + +// NewSparseR1CS is a constructor for SparseR1CS. It is meant to be use by gnark frontend only, +// and should not be used by gnark users. See plonk.NewCS(...) instead. +func NewSparseR1CS(capacity int) *SparseR1CS { + return newSystem(capacity, constraint.SystemSparseR1CS) +} + +func newSystem(capacity int, t constraint.SystemType) *system { + return &system{ + System: constraint.NewSystem(fr.Modulus(), capacity, t), + CoeffTable: newCoeffTable(capacity / 10), + } +} + +// Solve solves the constraint system with provided witness. +// If it's a R1CS returns R1CSSolution +// If it's a SparseR1CS returns SparseR1CSSolution +func (cs *system) Solve(witness witness.Witness, opts ...csolver.Option) (any, error) { + log := logger.Logger().With().Int("nbConstraints", cs.GetNbConstraints()).Logger() + start := time.Now() + + v := witness.Vector().(fr.Vector) + + // init the solver + solver, err := newSolver(cs, v, opts...) + if err != nil { + log.Err(err).Send() + return nil, err + } + + // reset the stateful blueprints + for i := range cs.Blueprints { + if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful[constraint.U32]); ok { + b.Reset() + } + } + + // defer log printing once all solver.values are computed + // (or sooner, if a constraint is not satisfied) + defer solver.printLogs(cs.Logs) + + // run it. + if err := solver.run(); err != nil { + log.Err(err).Send() + return nil, err + } + + log.Debug().Dur("took", time.Since(start)).Msg("constraint system solver done") + + // format the solution + // TODO @gbotrel revisit post-refactor + if cs.Type == constraint.SystemR1CS { + var res R1CSSolution + res.W = solver.values + res.A = solver.a + res.B = solver.b + res.C = solver.c + return &res, nil + } else { + // sparse R1CS + var res SparseR1CSSolution + // query l, r, o in Lagrange basis, not blinded + res.L, res.R, res.O = evaluateLROSmallDomain(cs, solver.values) + + return &res, nil + } + +} + +// IsSolved +// Deprecated: use _, err := Solve(...) instead +func (cs *system) IsSolved(witness witness.Witness, opts ...csolver.Option) error { + _, err := cs.Solve(witness, opts...) + return err +} + +// GetR1Cs return the list of R1C +func (cs *system) GetR1Cs() []constraint.R1C { + toReturn := make([]constraint.R1C, 0, cs.GetNbConstraints()) + + for _, inst := range cs.Instructions { + blueprint := cs.Blueprints[inst.BlueprintID] + if bc, ok := blueprint.(constraint.BlueprintR1C); ok { + var r1c constraint.R1C + bc.DecompressR1C(&r1c, inst.Unpack(&cs.System)) + toReturn = append(toReturn, r1c) + } + } + return toReturn +} + +// GetNbCoefficients return the number of unique coefficients needed in the R1CS +func (cs *system) GetNbCoefficients() int { + return len(cs.Coefficients) +} + +// CurveID returns curve ID as defined in gnark-crypto +func (cs *system) CurveID() ecc.ID { + return ecc.UNKNOWN +} + +func (cs *system) GetCoefficient(i int) (r constraint.U32) { + copy(r[:], cs.Coefficients[i][:]) + return +} + +// GetSparseR1Cs return the list of SparseR1C +func (cs *system) GetSparseR1Cs() []constraint.SparseR1C { + + toReturn := make([]constraint.SparseR1C, 0, cs.GetNbConstraints()) + + for _, inst := range cs.Instructions { + blueprint := cs.Blueprints[inst.BlueprintID] + if bc, ok := blueprint.(constraint.BlueprintSparseR1C); ok { + var sparseR1C constraint.SparseR1C + bc.DecompressSparseR1C(&sparseR1C, inst.Unpack(&cs.System)) + toReturn = append(toReturn, sparseR1C) + } + } + return toReturn +} + +// evaluateLROSmallDomain extracts the solver l, r, o, and returns it in lagrange form. +// solver = [ public | secret | internal ] +// TODO @gbotrel refactor; this seems to be a small util function for plonk +func evaluateLROSmallDomain(cs *system, solution []fr.Element) ([]fr.Element, []fr.Element, []fr.Element) { + + //s := int(pk.Domain[0].Cardinality) + s := cs.GetNbConstraints() + len(cs.Public) // len(spr.Public) is for the placeholder constraints + s = int(ecc.NextPowerOfTwo(uint64(s))) + + var l, r, o []fr.Element + l = make([]fr.Element, s, s+4) // +4 to leave room for the blinding in plonk + r = make([]fr.Element, s, s+4) + o = make([]fr.Element, s, s+4) + s0 := solution[0] + + for i := 0; i < len(cs.Public); i++ { // placeholders + l[i] = solution[i] + r[i] = s0 + o[i] = s0 + } + offset := len(cs.Public) + nbConstraints := cs.GetNbConstraints() + + var sparseR1C constraint.SparseR1C + j := 0 + for _, inst := range cs.Instructions { + blueprint := cs.Blueprints[inst.BlueprintID] + if bc, ok := blueprint.(constraint.BlueprintSparseR1C); ok { + bc.DecompressSparseR1C(&sparseR1C, inst.Unpack(&cs.System)) + + l[offset+j] = solution[sparseR1C.XA] + r[offset+j] = solution[sparseR1C.XB] + o[offset+j] = solution[sparseR1C.XC] + j++ + } + } + + offset += nbConstraints + + for i := 0; i < s-offset; i++ { // offset to reach 2**n constraints (where the id of l,r,o is 0, so we assign solver[0]) + l[offset+i] = s0 + r[offset+i] = s0 + o[offset+i] = s0 + } + + return l, r, o + +} + +// R1CSSolution represent a valid assignment to all the variables in the constraint system. +// The vector W such that Aw o Bw - Cw = 0 +type R1CSSolution struct { + W fr.Vector + A, B, C fr.Vector +} + +func (t *R1CSSolution) WriteTo(w io.Writer) (int64, error) { + n, err := t.W.WriteTo(w) + if err != nil { + return n, err + } + a, err := t.A.WriteTo(w) + n += a + if err != nil { + return n, err + } + a, err = t.B.WriteTo(w) + n += a + if err != nil { + return n, err + } + a, err = t.C.WriteTo(w) + n += a + return n, err +} + +func (t *R1CSSolution) ReadFrom(r io.Reader) (int64, error) { + n, err := t.W.ReadFrom(r) + if err != nil { + return n, err + } + a, err := t.A.ReadFrom(r) + n += a + if err != nil { + return n, err + } + a, err = t.B.ReadFrom(r) + n += a + if err != nil { + return n, err + } + a, err = t.C.ReadFrom(r) + n += a + return n, err +} + +// SparseR1CSSolution represent a valid assignment to all the variables in the constraint system. +type SparseR1CSSolution struct { + L, R, O fr.Vector +} + +func (t *SparseR1CSSolution) WriteTo(w io.Writer) (int64, error) { + n, err := t.L.WriteTo(w) + if err != nil { + return n, err + } + a, err := t.R.WriteTo(w) + n += a + if err != nil { + return n, err + } + a, err = t.O.WriteTo(w) + n += a + return n, err + +} + +func (t *SparseR1CSSolution) ReadFrom(r io.Reader) (int64, error) { + n, err := t.L.ReadFrom(r) + if err != nil { + return n, err + } + a, err := t.R.ReadFrom(r) + n += a + if err != nil { + return n, err + } + a, err = t.O.ReadFrom(r) + n += a + return n, err +} + +func (s *system) AddGkr(gkr gkrinfo.StoringInfo) error { + return s.System.AddGkr(gkr) +} diff --git a/constraint/marshal.go b/constraint/marshal.go index 06a0d2d6..a405799e 100644 --- a/constraint/marshal.go +++ b/constraint/marshal.go @@ -351,13 +351,20 @@ func getTagSet() cbor.TagSet { addType(reflect.TypeOf(BlueprintGenericHint{})) addType(reflect.TypeOf(BlueprintGenericR1C{})) - addType(reflect.TypeOf(BlueprintGenericSparseR1C{})) - addType(reflect.TypeOf(BlueprintSparseR1CAdd{})) - addType(reflect.TypeOf(BlueprintSparseR1CMul{})) - addType(reflect.TypeOf(BlueprintSparseR1CBool{})) - addType(reflect.TypeOf(BlueprintLookupHint{})) addType(reflect.TypeOf(Groth16Commitments{})) addType(reflect.TypeOf(PlonkCommitments{})) + addType(reflect.TypeOf(BlueprintGenericSparseR1C[U32]{})) + addType(reflect.TypeOf(BlueprintSparseR1CAdd[U32]{})) + addType(reflect.TypeOf(BlueprintSparseR1CMul[U32]{})) + addType(reflect.TypeOf(BlueprintSparseR1CBool[U32]{})) + addType(reflect.TypeOf(BlueprintLookupHint[U32]{})) + + addType(reflect.TypeOf(BlueprintGenericSparseR1C[U64]{})) + addType(reflect.TypeOf(BlueprintSparseR1CAdd[U64]{})) + addType(reflect.TypeOf(BlueprintSparseR1CMul[U64]{})) + addType(reflect.TypeOf(BlueprintSparseR1CBool[U64]{})) + addType(reflect.TypeOf(BlueprintLookupHint[U64]{})) + return ts } diff --git a/constraint/r1cs.go b/constraint/r1cs.go index 05b3fe8a..f343ad48 100644 --- a/constraint/r1cs.go +++ b/constraint/r1cs.go @@ -3,8 +3,8 @@ package constraint -type R1CS interface { - ConstraintSystem +type R1CS[E Element] interface { + ConstraintSystemGeneric[E] // AddR1C adds a constraint to the system and returns its id // This does not check for validity of the constraint. diff --git a/constraint/r1cs_sparse.go b/constraint/r1cs_sparse.go index 6d61d676..0d7b2256 100644 --- a/constraint/r1cs_sparse.go +++ b/constraint/r1cs_sparse.go @@ -3,8 +3,8 @@ package constraint -type SparseR1CS interface { - ConstraintSystem +type SparseR1CS[E Element] interface { + ConstraintSystemGeneric[E] // AddSparseR1C adds a constraint to the constraint system. AddSparseR1C(c SparseR1C, bID BlueprintID) int diff --git a/constraint/r1cs_sparse_test.go b/constraint/r1cs_sparse_test.go index 5a0bfd64..9a92f267 100644 --- a/constraint/r1cs_sparse_test.go +++ b/constraint/r1cs_sparse_test.go @@ -13,7 +13,7 @@ func ExampleSparseR1CS_GetSparseR1Cs() { // and build the linear expressions "manually". // note: R1CS apis are more mature; SparseR1CS apis are going to change in the next release(s). scs := cs.NewSparseR1CS(0) - blueprint := scs.AddBlueprint(&constraint.BlueprintGenericSparseR1C{}) + blueprint := scs.AddBlueprint(&constraint.BlueprintGenericSparseR1C[constraint.U64]{}) Y := scs.AddPublicVariable("Y") X := scs.AddSecretVariable("X") diff --git a/constraint/solver/gkrgates/registry.go b/constraint/solver/gkrgates/registry.go new file mode 100644 index 00000000..49610a17 --- /dev/null +++ b/constraint/solver/gkrgates/registry.go @@ -0,0 +1,263 @@ +// Package gkrgates contains the registry of GKR gates. +package gkrgates + +import ( + "fmt" + "reflect" + "runtime" + "sync" + + "github.com/consensys/gnark-crypto/ecc" + + bls12377 "github.com/consensys/gnark/internal/gkr/bls12-377" + bls12381 "github.com/consensys/gnark/internal/gkr/bls12-381" + bls24315 "github.com/consensys/gnark/internal/gkr/bls24-315" + bls24317 "github.com/consensys/gnark/internal/gkr/bls24-317" + bn254 "github.com/consensys/gnark/internal/gkr/bn254" + bw6633 "github.com/consensys/gnark/internal/gkr/bw6-633" + bw6761 "github.com/consensys/gnark/internal/gkr/bw6-761" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +var ( + gates = make(map[gkr.GateName]*gkrtypes.Gate) + gatesLock sync.Mutex +) + +type registerSettings struct { + solvableVar int + noSolvableVarVerification bool + noDegreeVerification bool + degree int + name gkr.GateName + curves []ecc.ID +} + +type registerOption func(*registerSettings) + +// WithSolvableVar gives the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will return an error if it cannot verify that this claim is correct. +func WithSolvableVar(solvableVar int) registerOption { + return func(settings *registerSettings) { + settings.solvableVar = solvableVar + } +} + +// WithUnverifiedSolvableVar sets the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not verify that the given index is correct. +func WithUnverifiedSolvableVar(solvableVar int) registerOption { + return func(settings *registerSettings) { + settings.noSolvableVarVerification = true + settings.solvableVar = solvableVar + } +} + +// WithNoSolvableVar sets the gate as having no variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// RegisterGate will not check the correctness of this claim. +func WithNoSolvableVar() registerOption { + return func(settings *registerSettings) { + settings.solvableVar = -1 + settings.noSolvableVarVerification = true + } +} + +// WithUnverifiedDegree sets the degree of the gate. RegisterGate will not verify that the given degree is correct. +func WithUnverifiedDegree(degree int) registerOption { + return func(settings *registerSettings) { + settings.noDegreeVerification = true + settings.degree = degree + } +} + +// WithDegree sets the degree of the gate. RegisterGate will return an error if the degree is not correct. +func WithDegree(degree int) registerOption { + return func(settings *registerSettings) { + settings.degree = degree + } +} + +// WithName can be used to set a human-readable name for the gate. +func WithName(name gkr.GateName) registerOption { + return func(settings *registerSettings) { + settings.name = name + } +} + +// WithCurves determines which curves the gate is validated on. +// The default is to validate on BN254. +// This works for most gates, unless the leading coefficient is divided by +// the curve's order, in which case the degree will be computed incorrectly. +func WithCurves(curves ...ecc.ID) registerOption { + return func(settings *registerSettings) { + settings.curves = curves + } +} + +// Register creates a gate object and stores it in the gates registry. +// - name is a human-readable name for the gate. +// - f is the polynomial function defining the gate. +// - nbIn is the number of inputs to the gate. +func Register(f gkr.GateFunction, nbIn int, options ...registerOption) error { + s := registerSettings{degree: -1, solvableVar: -1, name: GetDefaultGateName(f), curves: []ecc.ID{ecc.BN254}} + for _, option := range options { + option(&s) + } + + for _, curve := range s.curves { + gateVer, err := NewGateVerifier(curve) + if err != nil { + return err + } + + if s.degree == -1 { // find a degree + if s.noDegreeVerification { + panic("invalid settings") + } + const maxAutoDegreeBound = 32 + var err error + if s.degree, err = gateVer.findDegree(f, maxAutoDegreeBound, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", s.name, err) + } + } else { + if !s.noDegreeVerification { // check that the given degree is correct + if err = gateVer.verifyDegree(f, s.degree, nbIn); err != nil { + return fmt.Errorf("for gate %s: %v", s.name, err) + } + } + } + + if s.solvableVar == -1 { + if !s.noSolvableVarVerification { // find a solvable variable + s.solvableVar = gateVer.findSolvableVar(f, nbIn) + } + } else { + // solvable variable given + if !s.noSolvableVarVerification && !gateVer.isVarSolvable(f, s.solvableVar, nbIn) { + return fmt.Errorf("cannot verify the solvability of variable %d in gate %s", s.solvableVar, s.name) + } + } + + } + + gatesLock.Lock() + defer gatesLock.Unlock() + gates[s.name] = gkrtypes.NewGate(f, nbIn, s.degree, s.solvableVar) + return nil +} + +func Get(name gkr.GateName) *gkrtypes.Gate { + gatesLock.Lock() + defer gatesLock.Unlock() + if gate, ok := gates[name]; ok { + return gate + } + panic(fmt.Sprintf("gate \"%s\" not found", name)) +} + +type gateVerifier struct { + isAdditive func(f gkr.GateFunction, i int, nbIn int) bool + findDegree func(f gkr.GateFunction, max, nbIn int) (int, error) + verifyDegree func(f gkr.GateFunction, claimedDegree, nbIn int) error +} + +func NewGateVerifier(curve ecc.ID) (*gateVerifier, error) { + var ( + o gateVerifier + err error + ) + switch curve { + case ecc.BLS12_377: + o.isAdditive = bls12377.IsGateFunctionAdditive + o.findDegree = bls12377.FindGateFunctionDegree + o.verifyDegree = bls12377.VerifyGateFunctionDegree + case ecc.BLS12_381: + o.isAdditive = bls12381.IsGateFunctionAdditive + o.findDegree = bls12381.FindGateFunctionDegree + o.verifyDegree = bls12381.VerifyGateFunctionDegree + case ecc.BLS24_315: + o.isAdditive = bls24315.IsGateFunctionAdditive + o.findDegree = bls24315.FindGateFunctionDegree + o.verifyDegree = bls24315.VerifyGateFunctionDegree + case ecc.BLS24_317: + o.isAdditive = bls24317.IsGateFunctionAdditive + o.findDegree = bls24317.FindGateFunctionDegree + o.verifyDegree = bls24317.VerifyGateFunctionDegree + case ecc.BN254: + o.isAdditive = bn254.IsGateFunctionAdditive + o.findDegree = bn254.FindGateFunctionDegree + o.verifyDegree = bn254.VerifyGateFunctionDegree + case ecc.BW6_633: + o.isAdditive = bw6633.IsGateFunctionAdditive + o.findDegree = bw6633.FindGateFunctionDegree + o.verifyDegree = bw6633.VerifyGateFunctionDegree + case ecc.BW6_761: + o.isAdditive = bw6761.IsGateFunctionAdditive + o.findDegree = bw6761.FindGateFunctionDegree + o.verifyDegree = bw6761.VerifyGateFunctionDegree + default: + err = fmt.Errorf("unsupported curve %s", curve) + } + return &o, err +} + +// GetDefaultGateName provides a standardized name for a gate function, depending on its package and name. +// NB: For anonymous functions, the name is the same no matter the implicit arguments provided. +func GetDefaultGateName(fn gkr.GateFunction) gkr.GateName { + fnptr := reflect.ValueOf(fn).Pointer() + return gkr.GateName(runtime.FuncForPC(fnptr).Name()) +} + +// FindSolvableVar returns the index of a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns -1 if it fails to find one. +// nbIn is the number of inputs to the gate +func (v *gateVerifier) findSolvableVar(f gkr.GateFunction, nbIn int) int { + for i := range nbIn { + if v.isAdditive(f, i, nbIn) { + return i + } + } + return -1 +} + +// IsVarSolvable returns whether claimedSolvableVar is a variable whose value can be uniquely determined from that of the other variables along with the gate's output. +// It returns false if it fails to verify this claim. +// nbIn is the number of inputs to the gate. +func (v *gateVerifier) isVarSolvable(f gkr.GateFunction, claimedSolvableVar, nbIn int) bool { + return v.isAdditive(f, claimedSolvableVar, nbIn) +} + +func (v *gateVerifier) VerifyDegree(g *gkrtypes.Gate) error { + if err := v.verifyDegree(g.Evaluate, g.Degree(), g.NbIn()); err != nil { + deg, errFind := v.findDegree(g.Evaluate, g.Degree(), g.NbIn()) + if errFind != nil { + return fmt.Errorf("could not find gate degree: %w\n\tdegree verification error: %w", errFind, errFind) + } + return fmt.Errorf("detected degree %d\n\tdegree verification error: %w", deg, errFind) + } + return nil +} + +func (v *gateVerifier) VerifySolvability(g *gkrtypes.Gate) error { + if g.SolvableVar() == -1 { + return nil + } + if !v.isVarSolvable(g.Evaluate, g.SolvableVar(), g.NbIn()) { + return fmt.Errorf("cannot verify the solvability of variable %d", g.SolvableVar()) + } + return nil +} + +func init() { + // register some basic gates + gatesLock.Lock() + + gates[gkr.Identity] = gkrtypes.Identity() + gates[gkr.Add2] = gkrtypes.Add2() + gates[gkr.Sub2] = gkrtypes.Sub2() + gates[gkr.Neg] = gkrtypes.Neg() + gates[gkr.Mul2] = gkrtypes.Mul2() + + gatesLock.Unlock() +} diff --git a/constraint/solver/gkrgates/registry_test.go b/constraint/solver/gkrgates/registry_test.go new file mode 100644 index 00000000..ec41888e --- /dev/null +++ b/constraint/solver/gkrgates/registry_test.go @@ -0,0 +1,61 @@ +package gkrgates + +import ( + "fmt" + "testing" + + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/std/gkrapi/gkr" + "github.com/stretchr/testify/assert" +) + +func TestRegisterDegreeDetection(t *testing.T) { + testGate := func(name gkr.GateName, f gkr.GateFunction, nbIn, degree int) { + t.Run(string(name), func(t *testing.T) { + name = name + "-register-gate-test" + + assert.NoError(t, Register(f, nbIn, WithDegree(degree), WithName(name)), "given degree must be accepted") + + assert.Error(t, Register(f, nbIn, WithDegree(degree-1), WithName(name)), "lower degree must be rejected") + + assert.Error(t, Register(f, nbIn, WithDegree(degree+1), WithName(name)), "higher degree must be rejected") + + assert.NoError(t, Register(f, nbIn), "no degree must be accepted") + + assert.Equal(t, degree, Get(name).Degree(), "degree must be detected correctly") + }) + } + + testGate("select", func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + return x[0] + }, 3, 1) + + testGate("add3", func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + return api.Add(x[0], x[1], x[2]) + }, 3, 1) + + testGate("mul2", gkrtypes.Mul2().Evaluate, 2, 2) + + testGate("mimc", gkrtesting.NewCache().GetGate("mimc").Evaluate, 2, 7) + + testGate("sub2PlusOne", func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + return api.Sub( + api.Add(1, x[0]), + x[1], + ) + }, 2, 1) + + // zero polynomial must not be accepted + t.Run("zero", func(t *testing.T) { + const gateName gkr.GateName = "zero-register-gate-test" + expectedError := fmt.Errorf("for gate %s: %v", gateName, gkrtypes.ErrZeroFunction) + zeroGate := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + return api.Sub(x[0], x[0]) + } + assert.Equal(t, expectedError, Register(zeroGate, 1, WithName(gateName))) + + assert.Equal(t, expectedError, Register(zeroGate, 1, WithName(gateName), WithDegree(2))) + }) +} diff --git a/constraint/system.go b/constraint/system.go index e03586af..8542f4a6 100644 --- a/constraint/system.go +++ b/constraint/system.go @@ -6,13 +6,27 @@ import ( "github.com/consensys/gnark/backend/witness" "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/internal/gkr/gkrinfo" ) -// ConstraintSystem interface that all constraint systems implement. -type ConstraintSystem interface { +// ConstraintSystem is an interfaces that all constraint systems implement. This +// is the typed implementation using wide uint64 element representation, +// allowing to support all supported pairing based backends. +type ConstraintSystem = ConstraintSystemGeneric[U64] + +// ConstraintSystemU32 is an interfaces that all constraint systems implement. +// This is typed implementation for small field implementations. Small field +// implementations are not supported by pairing based backends, but can be +// exported for external use. +type ConstraintSystemU32 = ConstraintSystemGeneric[U32] + +// ConstraintSystemGeneric interface that all constraint systems implement. This is the +// generic interface, see the aliased specific implementations +// [ConstraintSystem] and [ConstraintSystemU32]. +type ConstraintSystemGeneric[E Element] interface { io.WriterTo io.ReaderFrom - Field + Field[E] Resolver CustomizableSystem @@ -52,17 +66,17 @@ type ConstraintSystem interface { AddCommitment(c Commitment) error GetCommitments() Commitments - AddGkr(gkr GkrInfo) error + AddGkr(gkr gkrinfo.StoringInfo) error AddLog(l LogEntry) // MakeTerm returns a new Term. The constraint system may store coefficients in a map, so // calls to this function will grow the memory usage of the constraint system. - MakeTerm(coeff Element, variableID int) Term + MakeTerm(coeff E, variableID int) Term // AddCoeff adds a coefficient to the underlying constraint system. The system will not store duplicate, // but is not purging for unused coeff either, so this grows memory usage. - AddCoeff(coeff Element) uint32 + AddCoeff(coeff E) uint32 NewDebugInfo(errName string, i ...interface{}) DebugInfo @@ -77,7 +91,7 @@ type ConstraintSystem interface { GetInstruction(int) Instruction - GetCoefficient(i int) Element + GetCoefficient(i int) E } type CustomizableSystem interface { diff --git a/constraint/tinyfield/coeff.go b/constraint/tinyfield/coeff.go index 9db4bed3..ecff5ff4 100644 --- a/constraint/tinyfield/coeff.go +++ b/constraint/tinyfield/coeff.go @@ -8,11 +8,12 @@ package cs import ( "encoding/binary" "errors" + "math/big" + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/internal/utils" - "math/big" - fr "github.com/consensys/gnark/internal/tinyfield" + fr "github.com/consensys/gnark/internal/smallfields/tinyfield" ) // CoeffTable ensure we store unique coefficients in the constraint system @@ -44,7 +45,7 @@ func (ct *CoeffTable) toBytes() []byte { buf = binary.LittleEndian.AppendUint64(buf, ctLen) for _, c := range ct.Coefficients { for _, w := range c { - buf = binary.LittleEndian.AppendUint64(buf, w) + buf = binary.LittleEndian.AppendUint32(buf, w) } } @@ -66,14 +67,14 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { var c fr.Element k := int(i) * fr.Bytes for j := 0; j < fr.Limbs; j++ { - c[j] = binary.LittleEndian.Uint64(buf[k+j*8 : k+(j+1)*8]) + c[j] = binary.LittleEndian.Uint32(buf[k+j*4 : k+(j+1)*4]) } ct.Coefficients[i] = c } return nil } -func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { +func (ct *CoeffTable) AddCoeff(coeff constraint.U32) uint32 { c := (*fr.Element)(coeff[:]) var cID uint32 if c.IsZero() { @@ -99,7 +100,7 @@ func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { return cID } -func (ct *CoeffTable) MakeTerm(coeff constraint.Element, variableID int) constraint.Term { +func (ct *CoeffTable) MakeTerm(coeff constraint.U32, variableID int) constraint.Term { cID := ct.AddCoeff(coeff) return constraint.Term{VID: uint32(variableID), CID: cID} } @@ -112,7 +113,7 @@ func (ct *CoeffTable) CoeffToString(cID int) string { // implements constraint.Field type field struct{} -var _ constraint.Field = &field{} +var _ constraint.Field[constraint.U32] = &field{} var ( two fr.Element @@ -128,7 +129,7 @@ func init() { minusTwo.Neg(&two) } -func (engine *field) FromInterface(i interface{}) constraint.Element { +func (engine *field) FromInterface(i interface{}) constraint.U32 { var e fr.Element if _, err := e.SetInterface(i); err != nil { // need to clean that --> some code path are dissimilar @@ -137,43 +138,43 @@ func (engine *field) FromInterface(i interface{}) constraint.Element { b := utils.FromInterface(i) e.SetBigInt(&b) } - var r constraint.Element + var r constraint.U32 copy(r[:], e[:]) return r } -func (engine *field) ToBigInt(c constraint.Element) *big.Int { +func (engine *field) ToBigInt(c constraint.U32) *big.Int { e := (*fr.Element)(c[:]) r := new(big.Int) e.BigInt(r) return r } -func (engine *field) Mul(a, b constraint.Element) constraint.Element { +func (engine *field) Mul(a, b constraint.U32) constraint.U32 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Mul(_a, _b) return a } -func (engine *field) Add(a, b constraint.Element) constraint.Element { +func (engine *field) Add(a, b constraint.U32) constraint.U32 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Add(_a, _b) return a } -func (engine *field) Sub(a, b constraint.Element) constraint.Element { +func (engine *field) Sub(a, b constraint.U32) constraint.U32 { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Sub(_a, _b) return a } -func (engine *field) Neg(a constraint.Element) constraint.Element { +func (engine *field) Neg(a constraint.U32) constraint.U32 { e := (*fr.Element)(a[:]) e.Neg(e) return a } -func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { +func (engine *field) Inverse(a constraint.U32) (constraint.U32, bool) { if a.IsZero() { return a, false } @@ -193,24 +194,24 @@ func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { return a, true } -func (engine *field) IsOne(a constraint.Element) bool { +func (engine *field) IsOne(a constraint.U32) bool { e := (*fr.Element)(a[:]) return e.IsOne() } -func (engine *field) One() constraint.Element { +func (engine *field) One() constraint.U32 { e := fr.One() - var r constraint.Element + var r constraint.U32 copy(r[:], e[:]) return r } -func (engine *field) String(a constraint.Element) string { +func (engine *field) String(a constraint.U32) string { e := (*fr.Element)(a[:]) return e.String() } -func (engine *field) Uint64(a constraint.Element) (uint64, bool) { +func (engine *field) Uint64(a constraint.U32) (uint64, bool) { e := (*fr.Element)(a[:]) if !e.IsUint64() { return 0, false diff --git a/constraint/tinyfield/r1cs_test.go b/constraint/tinyfield/r1cs_test.go index 6c94d940..d3466912 100644 --- a/constraint/tinyfield/r1cs_test.go +++ b/constraint/tinyfield/r1cs_test.go @@ -7,19 +7,22 @@ package cs_test import ( "bytes" + "reflect" + "testing" + + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/internal/backend/circuits" - "reflect" - "testing" + "github.com/consensys/gnark/internal/widecommitter" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" - "github.com/consensys/gnark/constraint/tinyfield" + cs "github.com/consensys/gnark/constraint/tinyfield" - fr "github.com/consensys/gnark/internal/tinyfield" + fr "github.com/consensys/gnark/internal/smallfields/tinyfield" ) func TestSerialization(t *testing.T) { @@ -32,8 +35,13 @@ func TestSerialization(t *testing.T) { if name == "range_constant" { return } + builder := r1cs.NewBuilder[constraint.U32] + if name == "commit" { + // smallfield builders do not support commitment. We use the wrapper which has the methods + builder = widecommitter.From(builder) + } - r1cs1, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs1, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -42,7 +50,7 @@ func TestSerialization(t *testing.T) { } // compile a second time to ensure determinism - r1cs2, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs2, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -149,7 +157,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("scs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), scs.NewBuilder, &c) + ccs, err := frontend.CompileGeneric[constraint.U32](fr.Modulus(), scs.NewBuilder, &c) if err != nil { b.Fatal(err) } @@ -163,7 +171,7 @@ func BenchmarkSolve(b *testing.B) { b.Run("r1cs", func(b *testing.B) { var c circuit - ccs, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) + ccs, err := frontend.CompileGeneric[constraint.U32](fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) if err != nil { b.Fatal(err) } diff --git a/constraint/tinyfield/solver.go b/constraint/tinyfield/solver.go index 9a276b5f..f36f8a25 100644 --- a/constraint/tinyfield/solver.go +++ b/constraint/tinyfield/solver.go @@ -8,11 +8,6 @@ package cs import ( "errors" "fmt" - "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/field/pool" - "github.com/consensys/gnark/constraint" - csolver "github.com/consensys/gnark/constraint/solver" - "github.com/rs/zerolog" "math" "math/big" "strconv" @@ -20,7 +15,13 @@ import ( "sync" "sync/atomic" - fr "github.com/consensys/gnark/internal/tinyfield" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/pool" + "github.com/consensys/gnark/constraint" + csolver "github.com/consensys/gnark/constraint/solver" + "github.com/rs/zerolog" + + fr "github.com/consensys/gnark/internal/smallfields/tinyfield" ) // solver represent the state of the solver during a call to System.Solve(...) @@ -325,18 +326,18 @@ func (solver *solver) divByCoeff(res *fr.Element, cID uint32) { } // Implement constraint.Solver -func (s *solver) GetValue(cID, vID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetValue(cID, vID uint32) constraint.U32 { + var r constraint.U32 e := s.computeTerm(constraint.Term{CID: cID, VID: vID}) copy(r[:], e[:]) return r } -func (s *solver) GetCoeff(cID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetCoeff(cID uint32) constraint.U32 { + var r constraint.U32 copy(r[:], s.Coefficients[cID][:]) return r } -func (s *solver) SetValue(vID uint32, f constraint.Element) { +func (s *solver) SetValue(vID uint32, f constraint.U32) { s.set(int(vID), *(*fr.Element)(f[:])) } @@ -346,7 +347,7 @@ func (s *solver) IsSolved(vID uint32) bool { // Read interprets input calldata as either a LinearExpression (if R1CS) or a Term (if Plonkish), // evaluates it and return the result and the number of uint32 word read. -func (s *solver) Read(calldata []uint32) (constraint.Element, int) { +func (s *solver) Read(calldata []uint32) (constraint.U32, int) { if s.Type == constraint.SystemSparseR1CS { if calldata[0] != 1 { panic("invalid calldata") @@ -362,7 +363,7 @@ func (s *solver) Read(calldata []uint32) (constraint.Element, int) { j += 2 } - var ret constraint.Element + var ret constraint.U32 copy(ret[:], r[:]) return ret, j } @@ -386,7 +387,7 @@ func (solver *solver) processInstruction(pi constraint.PackedInstruction, scratc } // blueprint declared "I know how to solve this." - if bc, ok := blueprint.(constraint.BlueprintSolvable); ok { + if bc, ok := blueprint.(constraint.BlueprintSolvable[constraint.U32]); ok { if err := bc.Solve(solver, inst); err != nil { return solver.wrapErrWithDebugInfo(cID, err) } diff --git a/constraint/tinyfield/system.go b/constraint/tinyfield/system.go index 6955d482..dcad6466 100644 --- a/constraint/tinyfield/system.go +++ b/constraint/tinyfield/system.go @@ -12,11 +12,12 @@ import ( "github.com/consensys/gnark/backend/witness" "github.com/consensys/gnark/constraint" csolver "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/internal/gkr/gkrinfo" "github.com/consensys/gnark/logger" "github.com/consensys/gnark-crypto/ecc" - fr "github.com/consensys/gnark/internal/tinyfield" + fr "github.com/consensys/gnark/internal/smallfields/tinyfield" ) type R1CS = system @@ -66,7 +67,7 @@ func (cs *system) Solve(witness witness.Witness, opts ...csolver.Option) (any, e // reset the stateful blueprints for i := range cs.Blueprints { - if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful); ok { + if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful[constraint.U32]); ok { b.Reset() } } @@ -135,7 +136,7 @@ func (cs *system) CurveID() ecc.ID { return ecc.UNKNOWN } -func (cs *system) GetCoefficient(i int) (r constraint.Element) { +func (cs *system) GetCoefficient(i int) (r constraint.U32) { copy(r[:], cs.Coefficients[i][:]) return } @@ -288,6 +289,6 @@ func (t *SparseR1CSSolution) ReadFrom(r io.Reader) (int64, error) { return n, err } -func (s *system) AddGkr(gkr constraint.GkrInfo) error { +func (s *system) AddGkr(gkr gkrinfo.StoringInfo) error { return s.System.AddGkr(gkr) } diff --git a/debug/symbol_table.go b/debug/symbol_table.go index dc25c3e8..538d442d 100644 --- a/debug/symbol_table.go +++ b/debug/symbol_table.go @@ -76,7 +76,7 @@ func (st *SymbolTable) CollectStack() []int { if strings.HasSuffix(function, "Define") { break } - if strings.HasSuffix(function, "callDeferred") { + if strings.HasSuffix(function, "callDeferred") || strings.HasSuffix(function, "callDeferred[...]") /* generic variant */ { break } } diff --git a/frontend/api.go b/frontend/api.go index 40a6fdfa..f548da49 100644 --- a/frontend/api.go +++ b/frontend/api.go @@ -124,7 +124,7 @@ type API interface { // [github.com/consensys/gnark/std/math/bits]. AssertIsLessOrEqual(v Variable, bound Variable) - // Println behaves like fmt.Println but accepts cd.Variable as parameter + // Println behaves like fmt.Println but accepts frontend.Variable as parameter // whose value will be resolved at runtime when computed by the solver Println(a ...Variable) diff --git a/frontend/builder.go b/frontend/builder.go index 40663985..f2058755 100644 --- a/frontend/builder.go +++ b/frontend/builder.go @@ -8,7 +8,25 @@ import ( "github.com/consensys/gnark/frontend/schema" ) -type NewBuilder func(*big.Int, CompileConfig) (Builder, error) +// NewBuilder is a function that creates a new constraint system builder for a +// given field. It takes a field modulus and a CompileConfig as arguments and +// returns a Builder interface and an error. The Builder interface provides +// methods for building and compiling the constraint system. +// +// gnark currently implements two builder constructors: +// - r1cs.NewBuilder +// - plonk.NewBuilder. +// +// For a constructor optimized for small field modulus, use [NewBuilderU32] instead. +type NewBuilder = NewBuilderGeneric[constraint.U64] + +// NewBuilderU32 is a function that creates a new constraint system builder +// for a given small field modulus. See [NewBuilder] for more details. +type NewBuilderU32 = NewBuilderGeneric[constraint.U32] + +// NewBuilderGeneric is a generic function that creates a new constraint system +// builder for a given field. See [NewBuilder] for more details. +type NewBuilderGeneric[E constraint.Element] func(*big.Int, CompileConfig) (Builder[E], error) // Compiler represents a constraint system compiler type Compiler interface { @@ -63,17 +81,15 @@ type Compiler interface { // ToCanonicalVariable converts a frontend.Variable to a constraint system specific Variable // ! Experimental: use in conjunction with constraint.CustomizableSystem ToCanonicalVariable(Variable) CanonicalVariable - - SetGkrInfo(constraint.GkrInfo) error } // Builder represents a constraint system builder -type Builder interface { +type Builder[E constraint.Element] interface { API Compiler // Compile is called after circuit.Define() to produce a final IR (ConstraintSystem) - Compile() (constraint.ConstraintSystem, error) + Compile() (constraint.ConstraintSystemGeneric[E], error) // PublicVariable is called by the compiler when parsing the circuit schema. It panics if // called inside circuit.Define() @@ -91,6 +107,26 @@ type Committer interface { Commit(toCommit ...Variable) (commitment Variable, err error) } +// WideCommitter allows to commit to the variables and returns the commitment as +// an extension field element. The commitment can be used as a challenge using +// Fiat-Shamir heuristic. This method is required when the circuit is defined +// over a small field where the individual commitment would be too small to +// achieve desired soundness level. +// +// This is experimental API and may be subject to change. It is not relevant for +// pairing-based backends where the commitment is in a large field and is not +// defined for such cases. Thus, the caller should check if this or [Committer] +// interfaces is implemented and use the appropriate method. +type WideCommitter interface { + // WideCommit commits to the variables and returns the commitments. + // This method is required when the circuit is defined over a small field + // where the individual commitment would be too small to achieve desired + // soundness level. + // + // The width parameter defines the number of elements in the commitment. + WideCommit(width int, toCommit ...Variable) (commitment []Variable, err error) +} + // Rangechecker allows to externally range-check the variables to be of // specified width. Not all compilers implement this interface. Users should // instead use [github.com/consensys/gnark/std/rangecheck] package which diff --git a/frontend/compile.go b/frontend/compile.go index 23110dda..d032fe8f 100644 --- a/frontend/compile.go +++ b/frontend/compile.go @@ -5,11 +5,14 @@ import ( "fmt" "math/big" "reflect" + "strings" + "github.com/consensys/gnark" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/debug" "github.com/consensys/gnark/frontend/schema" "github.com/consensys/gnark/internal/circuitdefer" + "github.com/consensys/gnark/internal/smallfields" "github.com/consensys/gnark/logger" ) @@ -31,9 +34,39 @@ import ( // if zkpID == backend.GROTH16 → R1CS // if zkpID == backend.PLONK → SparseR1CS // -// initialCapacity is an optional parameter that reserves memory in slices -// it should be set to the estimated number of constraints in the circuit, if known. +// For implementation which compiles the circuit optimized for a small-field modulus, see [CompileU32]. func Compile(field *big.Int, newBuilder NewBuilder, circuit Circuit, opts ...CompileOption) (constraint.ConstraintSystem, error) { + if !constraint.FitsElement[constraint.U64](field) { + var supported []string + for _, c := range gnark.Curves() { + supported = append(supported, c.String()) + } + return nil, fmt.Errorf("can not compile over field %s. This method supports compiling over scalar fields of supported curves: %s. For compiling over small fields use frontend.CompileU32", field, strings.Join(supported, ", ")) + } + return CompileGeneric(field, newBuilder, circuit, opts...) +} + +// CompileU32 is a variant of [Compile] which is optimized for small field +// modulus. +// +// NB! When compiling for a small field modulus, then the resulting [constraint.ConstraintSystem] is not +// compatible with pairing based backends. +func CompileU32(field *big.Int, newBuilder NewBuilderU32, circuit Circuit, opts ...CompileOption) (constraint.ConstraintSystemU32, error) { + if !constraint.FitsElement[constraint.U32](field) { + var supported []string + for _, c := range smallfields.Supported() { + supported = append(supported, c.String()) + } + return nil, fmt.Errorf("can not compile over field %s. This method only supports the following moduli: %s. For compiling over scalar fields of supported elliptic curves use frontend.Compile", field, strings.Join(supported, ", ")) + } + return CompileGeneric(field, newBuilder, circuit, opts...) +} + +// CompileGeneric is a generic version of [Compile] and [CompileU32]. It is +// mainly for allowing for type switching, for users the methods [Compile] and +// [CompileU32] are more convenient as are explicitly constrained to specific +// types. +func CompileGeneric[E constraint.Element](field *big.Int, newBuilder NewBuilderGeneric[E], circuit Circuit, opts ...CompileOption) (constraint.ConstraintSystemGeneric[E], error) { log := logger.Logger() log.Info().Msg("compiling circuit") // parse options @@ -64,13 +97,13 @@ func Compile(field *big.Int, newBuilder NewBuilder, circuit Circuit, opts ...Com return builder.Compile() } -func parseCircuit(builder Builder, circuit Circuit) (err error) { +func parseCircuit[E constraint.Element](builder Builder[E], circuit Circuit) (err error) { // ensure circuit.Define has pointer receiver if reflect.ValueOf(circuit).Kind() != reflect.Ptr { return errors.New("frontend.Circuit methods must be defined on pointer receiver") } - s, err := schema.Walk(circuit, tVariable, nil) + s, err := schema.Walk(builder.Field(), circuit, tVariable, nil) if err != nil { return err } @@ -101,13 +134,13 @@ func parseCircuit(builder Builder, circuit Circuit) (err error) { } // add public inputs first to compute correct offsets - _, err = schema.Walk(circuit, tVariable, variableAdder(schema.Public)) + _, err = schema.Walk(builder.Field(), circuit, tVariable, variableAdder(schema.Public)) if err != nil { return err } // add secret inputs - _, err = schema.Walk(circuit, tVariable, variableAdder(schema.Secret)) + _, err = schema.Walk(builder.Field(), circuit, tVariable, variableAdder(schema.Secret)) if err != nil { return err } @@ -130,7 +163,7 @@ func parseCircuit(builder Builder, circuit Circuit) (err error) { return } -func callDeferred(builder Builder) error { +func callDeferred[E constraint.Element](builder Builder[E]) error { for i := 0; i < len(circuitdefer.GetAll[func(API) error](builder)); i++ { if err := circuitdefer.GetAll[func(API) error](builder)[i](builder); err != nil { return fmt.Errorf("defer fn %d: %w", i, err) diff --git a/frontend/cs/r1cs/api.go b/frontend/cs/r1cs/api.go index c0763934..fe9485a7 100644 --- a/frontend/cs/r1cs/api.go +++ b/frontend/cs/r1cs/api.go @@ -11,7 +11,9 @@ import ( "runtime" "strings" + "github.com/consensys/gnark/internal/gkr/gkrinfo" "github.com/consensys/gnark/internal/hints" + "github.com/consensys/gnark/internal/smallfields" "github.com/consensys/gnark/internal/utils" @@ -30,13 +32,13 @@ import ( // Arithmetic // Add returns res = i1+i2+...in -func (builder *builder) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { +func (builder *builder[E]) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { // extract frontend.Variables from input vars, s := builder.toVariables(append([]frontend.Variable{i1, i2}, in...)...) return builder.add(vars, false, s, nil) } -func (builder *builder) MulAcc(a, b, c frontend.Variable) frontend.Variable { +func (builder *builder[E]) MulAcc(a, b, c frontend.Variable) frontend.Variable { // do the multiplication into builder.mbuf1 mulBC := func() { // reset the buffer @@ -74,28 +76,28 @@ func (builder *builder) MulAcc(a, b, c frontend.Variable) frontend.Variable { // copy _a in buffer, use _a as result; so if _a was already a linear expression and // results fits, _a is mutated without performing a new memalloc builder.mbuf2 = builder.mbuf2[:0] - builder.add([]expr.LinearExpression{_a, builder.mbuf1}, false, 0, &builder.mbuf2) + builder.add([]expr.LinearExpression[E]{_a, builder.mbuf1}, false, 0, &builder.mbuf2) _a = _a[:0] if len(builder.mbuf2) <= cap(_a) { // it fits, no mem alloc _a = append(_a, builder.mbuf2...) } else { // allocate an expression linear with extended capacity - _a = make(expr.LinearExpression, len(builder.mbuf2), len(builder.mbuf2)*3) + _a = make(expr.LinearExpression[E], len(builder.mbuf2), len(builder.mbuf2)*3) copy(_a, builder.mbuf2) } return _a } // Sub returns res = i1 - i2 -func (builder *builder) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { +func (builder *builder[E]) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { // extract frontend.Variables from input vars, s := builder.toVariables(append([]frontend.Variable{i1, i2}, in...)...) return builder.add(vars, true, s, nil) } // returns res = Σ(vars) or res = vars[0] - Σ(vars[1:]) if sub == true. -func (builder *builder) add(vars []expr.LinearExpression, sub bool, capacity int, res *expr.LinearExpression) frontend.Variable { +func (builder *builder[E]) add(vars []expr.LinearExpression[E], sub bool, capacity int, res *expr.LinearExpression[E]) frontend.Variable { // we want to merge all terms from input linear expressions // if they are duplicate, we reduce; that is, if multiple terms in different vars have the // same variable id. @@ -112,11 +114,12 @@ func (builder *builder) add(vars []expr.LinearExpression, sub bool, capacity int builder.heap.heapify() if res == nil { - t := make(expr.LinearExpression, 0, capacity) + t := make(expr.LinearExpression[E], 0, capacity) res = &t } curr := -1 + var zero E // process all the terms from all the inputs, in sorted order for len(builder.heap) > 0 { lID, tID := builder.heap[0].lID, builder.heap[0].tID @@ -130,7 +133,8 @@ func (builder *builder) add(vars []expr.LinearExpression, sub bool, capacity int builder.heap.fix(0) } t := &vars[lID][tID] - if t.Coeff.IsZero() { + + if t.Coeff == zero { // fast path to avoid function call overhead when calling t.Coeff.IsZero() continue // is this really needed? } if curr != -1 && t.VID == (*res)[curr].VID { @@ -157,7 +161,8 @@ func (builder *builder) add(vars []expr.LinearExpression, sub bool, capacity int if len((*res)) == 0 { // keep the linear expression valid (assertIsSet) - (*res) = append((*res), expr.NewTerm(0, constraint.Element{})) + var zero E + (*res) = append((*res), expr.NewTerm(0, zero)) } // if the linear expression LE is too long then record an equality // constraint LE * 1 = t and return short linear expression instead. @@ -172,7 +177,7 @@ func (builder *builder) add(vars []expr.LinearExpression, sub bool, capacity int } // Neg returns -i -func (builder *builder) Neg(i frontend.Variable) frontend.Variable { +func (builder *builder[E]) Neg(i frontend.Variable) frontend.Variable { v := builder.toVariable(i) if n, ok := builder.constantValue(v); ok { @@ -184,10 +189,10 @@ func (builder *builder) Neg(i frontend.Variable) frontend.Variable { } // Mul returns res = i1 * i2 * ... in -func (builder *builder) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { +func (builder *builder[E]) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { vars, _ := builder.toVariables(append([]frontend.Variable{i1, i2}, in...)...) - mul := func(v1, v2 expr.LinearExpression, first bool) expr.LinearExpression { + mul := func(v1, v2 expr.LinearExpression[E], first bool) expr.LinearExpression[E] { n1, v1Constant := builder.constantValue(v1) n2, v2Constant := builder.constantValue(v2) @@ -220,10 +225,10 @@ func (builder *builder) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) f return res } -func (builder *builder) mulConstant(v1 expr.LinearExpression, lambda constraint.Element, inPlace bool) expr.LinearExpression { +func (builder *builder[E]) mulConstant(v1 expr.LinearExpression[E], lambda E, inPlace bool) expr.LinearExpression[E] { // multiplying a frontend.Variable by a constant -> we updated the coefficients in the linear expression // leading to that frontend.Variable - var res expr.LinearExpression + var res expr.LinearExpression[E] if inPlace { res = v1 } else { @@ -236,7 +241,7 @@ func (builder *builder) mulConstant(v1 expr.LinearExpression, lambda constraint. return res } -func (builder *builder) DivUnchecked(i1, i2 frontend.Variable) frontend.Variable { +func (builder *builder[E]) DivUnchecked(i1, i2 frontend.Variable) frontend.Variable { vars, _ := builder.toVariables(i1, i2) v1 := vars[0] @@ -272,7 +277,7 @@ func (builder *builder) DivUnchecked(i1, i2 frontend.Variable) frontend.Variable } // Div returns res = i1 / i2 -func (builder *builder) Div(i1, i2 frontend.Variable) frontend.Variable { +func (builder *builder[E]) Div(i1, i2 frontend.Variable) frontend.Variable { vars, _ := builder.toVariables(i1, i2) v1 := vars[0] @@ -310,7 +315,7 @@ func (builder *builder) Div(i1, i2 frontend.Variable) frontend.Variable { } // Inverse returns res = inverse(v) -func (builder *builder) Inverse(i1 frontend.Variable) frontend.Variable { +func (builder *builder[E]) Inverse(i1 frontend.Variable) frontend.Variable { vars, _ := builder.toVariables(i1) if c, ok := builder.constantValue(vars[0]); ok { @@ -342,7 +347,7 @@ func (builder *builder) Inverse(i1 frontend.Variable) frontend.Variable { // n default value is fr.Bits the number of bits needed to represent a field element // // The result is in little endian (first bit= lsb) -func (builder *builder) ToBinary(i1 frontend.Variable, n ...int) []frontend.Variable { +func (builder *builder[E]) ToBinary(i1 frontend.Variable, n ...int) []frontend.Variable { // nbBits nbBits := builder.cs.FieldBitLen() if len(n) == 1 { @@ -356,12 +361,12 @@ func (builder *builder) ToBinary(i1 frontend.Variable, n ...int) []frontend.Vari } // FromBinary packs b, seen as a fr.Element in little endian -func (builder *builder) FromBinary(_b ...frontend.Variable) frontend.Variable { +func (builder *builder[E]) FromBinary(_b ...frontend.Variable) frontend.Variable { return bits.FromBinary(builder, _b) } // Xor compute the XOR between two frontend.Variables -func (builder *builder) Xor(_a, _b frontend.Variable) frontend.Variable { +func (builder *builder[E]) Xor(_a, _b frontend.Variable) frontend.Variable { vars, _ := builder.toVariables(_a, _b) @@ -389,7 +394,7 @@ func (builder *builder) Xor(_a, _b frontend.Variable) frontend.Variable { } // Or compute the OR between two frontend.Variables -func (builder *builder) Or(_a, _b frontend.Variable) frontend.Variable { +func (builder *builder[E]) Or(_a, _b frontend.Variable) frontend.Variable { vars, _ := builder.toVariables(_a, _b) a := vars[0] @@ -401,7 +406,7 @@ func (builder *builder) Or(_a, _b frontend.Variable) frontend.Variable { // the formulation used is for easing up the conversion to sparse r1cs res := builder.newInternalVariable() builder.MarkBoolean(res) - c := builder.Neg(res).(expr.LinearExpression) + c := builder.Neg(res).(expr.LinearExpression[E]) c = append(c, a...) c = append(c, b...) @@ -411,7 +416,7 @@ func (builder *builder) Or(_a, _b frontend.Variable) frontend.Variable { } // And compute the AND between two frontend.Variables -func (builder *builder) And(_a, _b frontend.Variable) frontend.Variable { +func (builder *builder[E]) And(_a, _b frontend.Variable) frontend.Variable { vars, _ := builder.toVariables(_a, _b) a := vars[0] @@ -430,7 +435,7 @@ func (builder *builder) And(_a, _b frontend.Variable) frontend.Variable { // Conditionals // Select if i0 is true, yields i1 else yields i2 -func (builder *builder) Select(i0, i1, i2 frontend.Variable) frontend.Variable { +func (builder *builder[E]) Select(i0, i1, i2 frontend.Variable) frontend.Variable { vars, _ := builder.toVariables(i0, i1, i2) cond := vars[0] @@ -473,7 +478,7 @@ func (builder *builder) Select(i0, i1, i2 frontend.Variable) frontend.Variable { // Lookup2 performs a 2-bit lookup between i1, i2, i3, i4 based on bits b0 // and b1. Returns i0 if b0=b1=0, i1 if b0=1 and b1=0, i2 if b0=0 and b1=1 // and i3 if b0=b1=1. -func (builder *builder) Lookup2(b0, b1 frontend.Variable, i0, i1, i2, i3 frontend.Variable) frontend.Variable { +func (builder *builder[E]) Lookup2(b0, b1 frontend.Variable, i0, i1, i2, i3 frontend.Variable) frontend.Variable { vars, _ := builder.toVariables(b0, b1, i0, i1, i2, i3) s0, s1 := vars[0], vars[1] in0, in1, in2, in3 := vars[2], vars[3], vars[4], vars[5] @@ -523,7 +528,7 @@ func (builder *builder) Lookup2(b0, b1 frontend.Variable, i0, i1, i2, i3 fronten } // IsZero returns 1 if i1 is zero, 0 otherwise -func (builder *builder) IsZero(i1 frontend.Variable) frontend.Variable { +func (builder *builder[E]) IsZero(i1 frontend.Variable) frontend.Variable { vars, _ := builder.toVariables(i1) a := vars[0] if c, ok := builder.constantValue(a); ok { @@ -563,7 +568,7 @@ func (builder *builder) IsZero(i1 frontend.Variable) frontend.Variable { } // Cmp returns 1 if i1>i2, 0 if i1=i2, -1 if i1 0 { sbb.WriteByte(' ') } - if v, ok := arg.(expr.LinearExpression); ok { + if v, ok := arg.(expr.LinearExpression[E]); ok { assertIsSet(v) sbb.WriteString("%s") @@ -628,9 +633,9 @@ func (builder *builder) Println(a ...frontend.Variable) { builder.cs.AddLog(log) } -func (builder *builder) printArg(log *constraint.LogEntry, sbb *strings.Builder, a frontend.Variable) { +func (builder *builder[E]) printArg(log *constraint.LogEntry, sbb *strings.Builder, a frontend.Variable) { - leafCount, err := schema.Walk(a, tVariable, nil) + leafCount, err := schema.Walk(builder.Field(), a, tVariable, nil) count := leafCount.Public + leafCount.Secret // no variables in nested struct, we use fmt std print function @@ -649,20 +654,20 @@ func (builder *builder) printArg(log *constraint.LogEntry, sbb *strings.Builder, sbb.WriteString(", ") } - v := tValue.Interface().(expr.LinearExpression) + v := tValue.Interface().(expr.LinearExpression[E]) // we set limits to the linear expression, so that the log printer // can evaluate it before printing it log.ToResolve = append(log.ToResolve, builder.getLinearExpression(v)) return nil } // ignoring error, printer() doesn't return errors - _, _ = schema.Walk(a, tVariable, printer) + _, _ = schema.Walk(builder.Field(), a, tVariable, printer) sbb.WriteByte('}') } // returns -le, the result is a copy -func (builder *builder) negateLinExp(l expr.LinearExpression) expr.LinearExpression { - res := make(expr.LinearExpression, len(l)) +func (builder *builder[E]) negateLinExp(l expr.LinearExpression[E]) expr.LinearExpression[E] { + res := make(expr.LinearExpression[E], len(l)) copy(res, l) for i := 0; i < len(res); i++ { res[i].Coeff = builder.cs.Neg(res[i].Coeff) @@ -670,12 +675,14 @@ func (builder *builder) negateLinExp(l expr.LinearExpression) expr.LinearExpress return res } -func (builder *builder) Compiler() frontend.Compiler { +func (builder *builder[E]) Compiler() frontend.Compiler { return builder } -func (builder *builder) Commit(v ...frontend.Variable) (frontend.Variable, error) { - +func (builder *builder[E]) Commit(v ...frontend.Variable) (frontend.Variable, error) { + if smallfields.IsSmallField(builder.Field()) { + return nil, fmt.Errorf("commitment not supported for small field %s", builder.Field()) + } // add a random mask to v { vCp := make([]frontend.Variable, len(v)+1) @@ -758,9 +765,9 @@ func (builder *builder) Commit(v ...frontend.Variable) (frontend.Variable, error // Cannot commit to a secret variable that has already been committed to // instead we commit to its commitment if committer := privateCommittedSeeker.Seek(t.VID); committer != -1 { - committerWireIndex := existingCommitmentIndexes[committer] // commit to this commitment instead - vars = append(vars, expr.LinearExpression{{Coeff: constraint.Element{1}, VID: committerWireIndex}}) // TODO Replace with mont 1 - builder.heap.push(linMeta{lID: len(vars) - 1, tID: 0, val: committerWireIndex}) // pushing to heap mid-op is okay because toCommit > t.VID > anything popped so far + committerWireIndex := existingCommitmentIndexes[committer] // commit to this commitment instead + vars = append(vars, expr.LinearExpression[E]{{Coeff: builder.cs.One(), VID: committerWireIndex}}) // TODO Replace with mont 1 + builder.heap.push(linMeta{lID: len(vars) - 1, tID: 0, val: committerWireIndex}) // pushing to heap mid-op is okay because toCommit > t.VID > anything popped so far continue } @@ -796,7 +803,7 @@ func (builder *builder) Commit(v ...frontend.Variable) (frontend.Variable, error res := hintOut[0] - commitment.CommitmentIndex = (res.(expr.LinearExpression))[0].WireID() + commitment.CommitmentIndex = (res.(expr.LinearExpression[E]))[0].WireID() if err := builder.cs.AddCommitment(commitment); err != nil { return nil, err @@ -805,7 +812,7 @@ func (builder *builder) Commit(v ...frontend.Variable) (frontend.Variable, error return res, nil } -func (builder *builder) wireIDsToVars(wireIDs ...[]int) []frontend.Variable { +func (builder *builder[E]) wireIDsToVars(wireIDs ...[]int) []frontend.Variable { n := 0 for i := range wireIDs { n += len(wireIDs[i]) @@ -821,6 +828,6 @@ func (builder *builder) wireIDsToVars(wireIDs ...[]int) []frontend.Variable { return res } -func (builder *builder) SetGkrInfo(info constraint.GkrInfo) error { +func (builder *builder[E]) SetGkrInfo(info gkrinfo.StoringInfo) error { return builder.cs.AddGkr(info) } diff --git a/frontend/cs/r1cs/api_assertions.go b/frontend/cs/r1cs/api_assertions.go index 3c32a1bf..660b3d21 100644 --- a/frontend/cs/r1cs/api_assertions.go +++ b/frontend/cs/r1cs/api_assertions.go @@ -14,7 +14,7 @@ import ( ) // AssertIsEqual adds an assertion in the constraint builder (i1 == i2) -func (builder *builder) AssertIsEqual(i1, i2 frontend.Variable) { +func (builder *builder[E]) AssertIsEqual(i1, i2 frontend.Variable) { c1, i1Constant := builder.constantValue(i1) c2, i2Constant := builder.constantValue(i2) @@ -37,8 +37,8 @@ func (builder *builder) AssertIsEqual(i1, i2 frontend.Variable) { } // AssertIsDifferent constrain i1 and i2 to be different -func (builder *builder) AssertIsDifferent(i1, i2 frontend.Variable) { - s := builder.Sub(i1, i2).(expr.LinearExpression) +func (builder *builder[E]) AssertIsDifferent(i1, i2 frontend.Variable) { + s := builder.Sub(i1, i2).(expr.LinearExpression[E]) if len(s) == 1 && s[0].Coeff.IsZero() { panic("AssertIsDifferent(x,x) will never be satisfied") } @@ -47,7 +47,7 @@ func (builder *builder) AssertIsDifferent(i1, i2 frontend.Variable) { } // AssertIsBoolean adds an assertion in the constraint builder (v == 0 ∥ v == 1) -func (builder *builder) AssertIsBoolean(i1 frontend.Variable) { +func (builder *builder[E]) AssertIsBoolean(i1 frontend.Variable) { v := builder.toVariable(i1) @@ -77,7 +77,7 @@ func (builder *builder) AssertIsBoolean(i1 frontend.Variable) { } } -func (builder *builder) AssertIsCrumb(i1 frontend.Variable) { +func (builder *builder[E]) AssertIsCrumb(i1 frontend.Variable) { i1 = builder.MulAcc(builder.Mul(-3, i1), i1, i1) i1 = builder.MulAcc(builder.Mul(2, i1), i1, i1) builder.AssertIsEqual(i1, 0) @@ -89,30 +89,28 @@ func (builder *builder) AssertIsCrumb(i1 frontend.Variable) { // // derived from: // https://github.com/zcash/zips/blob/main/protocol/protocol.pdf -func (builder *builder) AssertIsLessOrEqual(v frontend.Variable, bound frontend.Variable) { +func (builder *builder[E]) AssertIsLessOrEqual(v frontend.Variable, bound frontend.Variable) { cv, vConst := builder.constantValue(v) cb, bConst := builder.constantValue(bound) - // both inputs are constants - if vConst && bConst { + switch { + case vConst && bConst: // both inputs are constants bv, bb := builder.cs.ToBigInt(cv), builder.cs.ToBigInt(cb) if bv.Cmp(bb) == 1 { panic(fmt.Sprintf("AssertIsLessOrEqual: %s > %s", bv.String(), bb.String())) } - } - - // bound is constant - if bConst { + return + case bConst: // bound is constant nbBits := builder.cs.FieldBitLen() vBits := bits.ToBinary(builder, v, bits.WithNbDigits(nbBits), bits.WithUnconstrainedOutputs()) builder.MustBeLessOrEqCst(vBits, builder.cs.ToBigInt(cb), v) return + default: + builder.mustBeLessOrEqVar(v, bound) } - - builder.mustBeLessOrEqVar(v, bound) } -func (builder *builder) mustBeLessOrEqVar(a, bound frontend.Variable) { +func (builder *builder[E]) mustBeLessOrEqVar(a, bound frontend.Variable) { // here bound is NOT a constant, // but a can be either constant or a wire. @@ -161,7 +159,7 @@ func (builder *builder) mustBeLessOrEqVar(a, bound frontend.Variable) { // MustBeLessOrEqCst asserts that value represented using its bit decomposition // aBits is less or equal than constant bound. The method boolean constraints // the bits in aBits, so the caller can provide unconstrained bits. -func (builder *builder) MustBeLessOrEqCst(aBits []frontend.Variable, bound *big.Int, aForDebug frontend.Variable) { +func (builder *builder[E]) MustBeLessOrEqCst(aBits []frontend.Variable, bound *big.Int, aForDebug frontend.Variable) { nbBits := builder.cs.FieldBitLen() if len(aBits) > nbBits { diff --git a/frontend/cs/r1cs/builder.go b/frontend/cs/r1cs/builder.go index f13526b8..f84db2a9 100644 --- a/frontend/cs/r1cs/builder.go +++ b/frontend/cs/r1cs/builder.go @@ -10,6 +10,8 @@ import ( "sort" "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/babybear" + "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/debug" "github.com/consensys/gnark/frontend" @@ -18,10 +20,11 @@ import ( "github.com/consensys/gnark/internal/circuitdefer" "github.com/consensys/gnark/internal/frontendtype" "github.com/consensys/gnark/internal/kvstore" - "github.com/consensys/gnark/internal/tinyfield" + "github.com/consensys/gnark/internal/smallfields/tinyfield" "github.com/consensys/gnark/internal/utils" "github.com/consensys/gnark/logger" + babybearr1cs "github.com/consensys/gnark/constraint/babybear" bls12377r1cs "github.com/consensys/gnark/constraint/bls12-377" bls12381r1cs "github.com/consensys/gnark/constraint/bls12-381" bls24315r1cs "github.com/consensys/gnark/constraint/bls24-315" @@ -29,51 +32,52 @@ import ( bn254r1cs "github.com/consensys/gnark/constraint/bn254" bw6633r1cs "github.com/consensys/gnark/constraint/bw6-633" bw6761r1cs "github.com/consensys/gnark/constraint/bw6-761" + koalabearr1cs "github.com/consensys/gnark/constraint/koalabear" "github.com/consensys/gnark/constraint/solver" tinyfieldr1cs "github.com/consensys/gnark/constraint/tinyfield" ) -// NewBuilder returns a new R1CS builder which implements frontend.API. +// NewBuilder returns a new R1CS builder which implements [frontend.API]. // Additionally, this builder also implements [frontend.Committer]. -func NewBuilder(field *big.Int, config frontend.CompileConfig) (frontend.Builder, error) { - return newBuilder(field, config), nil +func NewBuilder[E constraint.Element](field *big.Int, config frontend.CompileConfig) (frontend.Builder[E], error) { + return newBuilder[E](field, config), nil } -type builder struct { - cs constraint.R1CS +type builder[E constraint.Element] struct { + cs constraint.R1CS[E] config frontend.CompileConfig kvstore.Store // map for recording boolean constrained variables (to not constrain them twice) - mtBooleans map[[16]byte][]expr.LinearExpression + mtBooleans map[[16]byte][]expr.LinearExpression[E] - tOne constraint.Element - eZero, eOne expr.LinearExpression + tOne E + eZero, eOne expr.LinearExpression[E] cZero, cOne constraint.LinearExpression // helps merge k sorted linear expressions heap minHeap // buffers used to do in place api.MAC - mbuf1 expr.LinearExpression - mbuf2 expr.LinearExpression + mbuf1 expr.LinearExpression[E] + mbuf2 expr.LinearExpression[E] genericGate constraint.BlueprintID } // initialCapacity has quite some impact on frontend performance, especially on large circuits size // we may want to add build tags to tune that -func newBuilder(field *big.Int, config frontend.CompileConfig) *builder { +func newBuilder[E constraint.Element](field *big.Int, config frontend.CompileConfig) *builder[E] { macCapacity := 100 if config.CompressThreshold != 0 { macCapacity = config.CompressThreshold } - builder := builder{ - mtBooleans: make(map[[16]byte][]expr.LinearExpression, config.Capacity/10), + bldr := &builder[E]{ + mtBooleans: make(map[[16]byte][]expr.LinearExpression[E], config.Capacity/10), config: config, heap: make(minHeap, 0, 100), - mbuf1: make(expr.LinearExpression, 0, macCapacity), - mbuf2: make(expr.LinearExpression, 0, macCapacity), + mbuf1: make(expr.LinearExpression[E], 0, macCapacity), + mbuf2: make(expr.LinearExpression[E], 0, macCapacity), Store: kvstore.New(), } @@ -81,87 +85,106 @@ func newBuilder(field *big.Int, config frontend.CompileConfig) *builder { curve := utils.FieldToCurve(field) - switch curve { - case ecc.BLS12_377: - builder.cs = bls12377r1cs.NewR1CS(config.Capacity) - case ecc.BLS12_381: - builder.cs = bls12381r1cs.NewR1CS(config.Capacity) - case ecc.BN254: - builder.cs = bn254r1cs.NewR1CS(config.Capacity) - case ecc.BW6_761: - builder.cs = bw6761r1cs.NewR1CS(config.Capacity) - case ecc.BW6_633: - builder.cs = bw6633r1cs.NewR1CS(config.Capacity) - case ecc.BLS24_315: - builder.cs = bls24315r1cs.NewR1CS(config.Capacity) - case ecc.BLS24_317: - builder.cs = bls24317r1cs.NewR1CS(config.Capacity) - default: - if field.Cmp(tinyfield.Modulus()) == 0 { - builder.cs = tinyfieldr1cs.NewR1CS(config.Capacity) - break + switch bldrT := any(bldr).(type) { + case *builder[constraint.U64]: + switch curve { + case ecc.BLS12_377: + bldrT.cs = bls12377r1cs.NewR1CS(config.Capacity) + case ecc.BLS12_381: + bldrT.cs = bls12381r1cs.NewR1CS(config.Capacity) + case ecc.BN254: + bldrT.cs = bn254r1cs.NewR1CS(config.Capacity) + case ecc.BW6_761: + bldrT.cs = bw6761r1cs.NewR1CS(config.Capacity) + case ecc.BW6_633: + bldrT.cs = bw6633r1cs.NewR1CS(config.Capacity) + case ecc.BLS24_315: + bldrT.cs = bls24315r1cs.NewR1CS(config.Capacity) + case ecc.BLS24_317: + bldrT.cs = bls24317r1cs.NewR1CS(config.Capacity) + default: + panic("not implemented") + } + case *builder[constraint.U32]: + switch curve { + default: + if field.Cmp(tinyfield.Modulus()) == 0 { + bldrT.cs = tinyfieldr1cs.NewR1CS(config.Capacity) + break + } + if field.Cmp(babybear.Modulus()) == 0 { + bldrT.cs = babybearr1cs.NewR1CS(config.Capacity) + break + } + if field.Cmp(koalabear.Modulus()) == 0 { + bldrT.cs = koalabearr1cs.NewR1CS(config.Capacity) + break + } + panic("not implemented") } - panic("not implemented") + default: + panic("invalid constraint.Element type") } - builder.tOne = builder.cs.One() - builder.cs.AddPublicVariable("1") + bldr.tOne = bldr.cs.One() + bldr.cs.AddPublicVariable("1") - builder.genericGate = builder.cs.AddBlueprint(&constraint.BlueprintGenericR1C{}) + bldr.genericGate = bldr.cs.AddBlueprint(&constraint.BlueprintGenericR1C{}) - builder.eZero = expr.NewLinearExpression(0, constraint.Element{}) - builder.eOne = expr.NewLinearExpression(0, builder.tOne) + var zero E + bldr.eZero = expr.NewLinearExpression(0, zero) + bldr.eOne = expr.NewLinearExpression(0, bldr.tOne) - builder.cOne = constraint.LinearExpression{constraint.Term{VID: 0, CID: constraint.CoeffIdOne}} - builder.cZero = constraint.LinearExpression{constraint.Term{VID: 0, CID: constraint.CoeffIdZero}} + bldr.cOne = constraint.LinearExpression{constraint.Term{VID: 0, CID: constraint.CoeffIdOne}} + bldr.cZero = constraint.LinearExpression{constraint.Term{VID: 0, CID: constraint.CoeffIdZero}} - return &builder + return bldr } // newInternalVariable creates a new wire, appends it on the list of wires of the circuit, sets // the wire's id to the number of wires, and returns it -func (builder *builder) newInternalVariable() expr.LinearExpression { +func (builder *builder[E]) newInternalVariable() expr.LinearExpression[E] { idx := builder.cs.AddInternalVariable() return expr.NewLinearExpression(idx, builder.tOne) } // PublicVariable creates a new public Variable -func (builder *builder) PublicVariable(f schema.LeafInfo) frontend.Variable { +func (builder *builder[E]) PublicVariable(f schema.LeafInfo) frontend.Variable { idx := builder.cs.AddPublicVariable(f.FullName()) return expr.NewLinearExpression(idx, builder.tOne) } // SecretVariable creates a new secret Variable -func (builder *builder) SecretVariable(f schema.LeafInfo) frontend.Variable { +func (builder *builder[E]) SecretVariable(f schema.LeafInfo) frontend.Variable { idx := builder.cs.AddSecretVariable(f.FullName()) return expr.NewLinearExpression(idx, builder.tOne) } // cstOne return the one constant -func (builder *builder) cstOne() expr.LinearExpression { +func (builder *builder[E]) cstOne() expr.LinearExpression[E] { return builder.eOne } // cstZero return the zero constant -func (builder *builder) cstZero() expr.LinearExpression { +func (builder *builder[E]) cstZero() expr.LinearExpression[E] { return builder.eZero } -func (builder *builder) isCstOne(c constraint.Element) bool { +func (builder *builder[E]) isCstOne(c E) bool { return builder.cs.IsOne(c) } -func (builder *builder) Field() *big.Int { +func (builder *builder[E]) Field() *big.Int { return builder.cs.Field() } -func (builder *builder) FieldBitLen() int { +func (builder *builder[E]) FieldBitLen() int { return builder.cs.FieldBitLen() } // newR1C clones the linear expression associated with the Variables (to avoid offsetting the ID multiple time) // and return a R1C -func (builder *builder) newR1C(l, r, o frontend.Variable) constraint.R1C { +func (builder *builder[E]) newR1C(l, r, o frontend.Variable) constraint.R1C { L := builder.getLinearExpression(l) R := builder.getLinearExpression(r) O := builder.getLinearExpression(o) @@ -180,10 +203,10 @@ func (builder *builder) newR1C(l, r, o frontend.Variable) constraint.R1C { return constraint.R1C{L: L, R: R, O: O} } -func (builder *builder) getLinearExpression(_l interface{}) constraint.LinearExpression { +func (builder *builder[E]) getLinearExpression(_l interface{}) constraint.LinearExpression { var L constraint.LinearExpression switch tl := _l.(type) { - case expr.LinearExpression: + case expr.LinearExpression[E]: if len(tl) == 1 && tl[0].VID == 0 { if tl[0].Coeff.IsZero() { return builder.cZero @@ -207,7 +230,7 @@ func (builder *builder) getLinearExpression(_l interface{}) constraint.LinearExp // MarkBoolean sets (but do not **constraint**!) v to be boolean // This is useful in scenarios where a variable is known to be boolean through a constraint // that is not api.AssertIsBoolean. If v is a constant, this is a no-op. -func (builder *builder) MarkBoolean(v frontend.Variable) { +func (builder *builder[E]) MarkBoolean(v frontend.Variable) { if b, ok := builder.constantValue(v); ok { if !(b.IsZero() || builder.isCstOne(b)) { panic("MarkBoolean called a non-boolean constant") @@ -215,7 +238,7 @@ func (builder *builder) MarkBoolean(v frontend.Variable) { return } // v is a linear expression - l := v.(expr.LinearExpression) + l := v.(expr.LinearExpression[E]) sort.Sort(l) key := l.HashCode() @@ -227,12 +250,12 @@ func (builder *builder) MarkBoolean(v frontend.Variable) { // IsBoolean returns true if given variable was marked as boolean in the compiler (see MarkBoolean) // Use with care; variable may not have been **constrained** to be boolean // This returns true if the v is a constant and v == 0 || v == 1. -func (builder *builder) IsBoolean(v frontend.Variable) bool { +func (builder *builder[E]) IsBoolean(v frontend.Variable) bool { if b, ok := builder.constantValue(v); ok { return (b.IsZero() || builder.isCstOne(b)) } // v is a linear expression - l := v.(expr.LinearExpression) + l := v.(expr.LinearExpression[E]) sort.Sort(l) key := l.HashCode() @@ -256,7 +279,7 @@ func init() { } // Compile constructs a rank-1 constraint system -func (builder *builder) Compile() (constraint.ConstraintSystem, error) { +func (builder *builder[E]) Compile() (constraint.ConstraintSystemGeneric[E], error) { // TODO if already compiled, return builder.cs object log := logger.Logger() log.Info(). @@ -276,7 +299,7 @@ func (builder *builder) Compile() (constraint.ConstraintSystem, error) { // ConstantValue returns the big.Int value of v. // Will panic if v.IsConstant() == false -func (builder *builder) ConstantValue(v frontend.Variable) (*big.Int, bool) { +func (builder *builder[E]) ConstantValue(v frontend.Variable) (*big.Int, bool) { coeff, ok := builder.constantValue(v) if !ok { return nil, false @@ -284,20 +307,21 @@ func (builder *builder) ConstantValue(v frontend.Variable) (*big.Int, bool) { return builder.cs.ToBigInt(coeff), true } -func (builder *builder) constantValue(v frontend.Variable) (constraint.Element, bool) { - if _v, ok := v.(expr.LinearExpression); ok { +func (builder *builder[E]) constantValue(v frontend.Variable) (E, bool) { + var zero E + if _v, ok := v.(expr.LinearExpression[E]); ok { assertIsSet(_v) if len(_v) != 1 { // TODO @gbotrel this assumes linear expressions of coeff are not possible // and are always reduced to one element. may not always be true? - return constraint.Element{}, false + return zero, false } - if _v[0].Coeff.IsZero() { - return constraint.Element{}, true + if _v[0].Coeff == zero { // fast path for zero comparison to avoid overhead of calling IsZero + return zero, true } if !(_v[0].WireID() == 0) { // public ONE WIRE - return constraint.Element{}, false + return zero, false } return _v[0].Coeff, true } @@ -308,19 +332,19 @@ func (builder *builder) constantValue(v frontend.Variable) (constraint.Element, // // if input is already a linearExpression, does nothing // else, attempts to convert input to a big.Int (see utils.FromInterface) and returns a toVariable linearExpression -func (builder *builder) toVariable(input interface{}) expr.LinearExpression { +func (builder *builder[E]) toVariable(input interface{}) expr.LinearExpression[E] { switch t := input.(type) { - case expr.LinearExpression: + case expr.LinearExpression[E]: // this is already a "kwown" variable assertIsSet(t) return t - case *expr.LinearExpression: + case *expr.LinearExpression[E]: assertIsSet(*t) return *t - case constraint.Element: + case E: return expr.NewLinearExpression(0, t) - case *constraint.Element: + case *E: return expr.NewLinearExpression(0, *t) default: // try to make it into a constant @@ -330,8 +354,8 @@ func (builder *builder) toVariable(input interface{}) expr.LinearExpression { } // toVariables return frontend.Variable corresponding to inputs and the total size of the linear expressions -func (builder *builder) toVariables(in ...frontend.Variable) ([]expr.LinearExpression, int) { - r := make([]expr.LinearExpression, 0, len(in)) +func (builder *builder[E]) toVariables(in ...frontend.Variable) ([]expr.LinearExpression[E], int) { + r := make([]expr.LinearExpression[E], 0, len(in)) s := 0 e := func(i frontend.Variable) { v := builder.toVariable(i) @@ -358,21 +382,21 @@ func (builder *builder) toVariables(in ...frontend.Variable) ([]expr.LinearExpre // // No new constraints are added to the newly created wire and must be added // manually in the circuit. Failing to do so leads to solver failure. -func (builder *builder) NewHint(f solver.Hint, nbOutputs int, inputs ...frontend.Variable) ([]frontend.Variable, error) { +func (builder *builder[E]) NewHint(f solver.Hint, nbOutputs int, inputs ...frontend.Variable) ([]frontend.Variable, error) { return builder.newHint(f, solver.GetHintID(f), nbOutputs, inputs) } -func (builder *builder) NewHintForId(id solver.HintID, nbOutputs int, inputs ...frontend.Variable) ([]frontend.Variable, error) { +func (builder *builder[E]) NewHintForId(id solver.HintID, nbOutputs int, inputs ...frontend.Variable) ([]frontend.Variable, error) { return builder.newHint(nil, id, nbOutputs, inputs) } -func (builder *builder) newHint(f solver.Hint, id solver.HintID, nbOutputs int, inputs []frontend.Variable) ([]frontend.Variable, error) { +func (builder *builder[E]) newHint(f solver.Hint, id solver.HintID, nbOutputs int, inputs []frontend.Variable) ([]frontend.Variable, error) { hintInputs := make([]constraint.LinearExpression, len(inputs)) // TODO @gbotrel hint input pass // ensure inputs are set and pack them in a []uint64 for i, in := range inputs { - if t, ok := in.(expr.LinearExpression); ok { + if t, ok := in.(expr.LinearExpression[E]); ok { assertIsSet(t) hintInputs[i] = builder.getLinearExpression(t) } else { @@ -401,7 +425,7 @@ func (builder *builder) newHint(f solver.Hint, id solver.HintID, nbOutputs int, // var a variable // cs.Mul(a, 1) // since a was not in the circuit struct it is not a secret variable -func assertIsSet(l expr.LinearExpression) { +func assertIsSet[E constraint.Element](l expr.LinearExpression[E]) { if len(l) == 0 { // errNoValue triggered when trying to access a variable that was not allocated errNoValue := errors.New("can't determine API input value") @@ -420,23 +444,23 @@ func assertIsSet(l expr.LinearExpression) { // something more like builder.sprintf("my message %le %lv", l0, l1) // to build logs for both debug and println // and append some program location.. (see other todo in debug_info.go) -func (builder *builder) newDebugInfo(errName string, in ...interface{}) constraint.DebugInfo { +func (builder *builder[E]) newDebugInfo(errName string, in ...interface{}) constraint.DebugInfo { for i := 0; i < len(in); i++ { // for inputs that are LinearExpressions or Term, we need to "Make" them in the backend. // TODO @gbotrel this is a duplicate effort with adding a constraint and should be taken care off switch t := in[i].(type) { - case *expr.LinearExpression: + case *expr.LinearExpression[E]: in[i] = builder.getLinearExpression(*t) - case expr.LinearExpression: + case expr.LinearExpression[E]: in[i] = builder.getLinearExpression(t) - case expr.Term: - in[i] = builder.getLinearExpression(expr.LinearExpression{t}) - case *expr.Term: - in[i] = builder.getLinearExpression(expr.LinearExpression{*t}) - case constraint.Element: + case expr.Term[E]: + in[i] = builder.getLinearExpression(expr.LinearExpression[E]{t}) + case *expr.Term[E]: + in[i] = builder.getLinearExpression(expr.LinearExpression[E]{*t}) + case E: in[i] = builder.cs.String(t) - case *constraint.Element: + case *E: in[i] = builder.cs.String(*t) } } @@ -449,7 +473,7 @@ func (builder *builder) newDebugInfo(errName string, in ...interface{}) constrai // equal than CompressThreshold in the configuration, replaces it with a linear // expression of one term. In that case it adds an equality constraint enforcing // the correctness of the returned linear expression. -func (builder *builder) compress(le expr.LinearExpression) expr.LinearExpression { +func (builder *builder[E]) compress(le expr.LinearExpression[E]) expr.LinearExpression[E] { if builder.config.CompressThreshold <= 0 || len(le) < builder.config.CompressThreshold { return le } @@ -460,32 +484,32 @@ func (builder *builder) compress(le expr.LinearExpression) expr.LinearExpression return t } -func (builder *builder) Defer(cb func(frontend.API) error) { +func (builder *builder[E]) Defer(cb func(frontend.API) error) { circuitdefer.Put(builder, cb) } -func (*builder) FrontendType() frontendtype.Type { +func (*builder[E]) FrontendType() frontendtype.Type { return frontendtype.R1CS } // AddInstruction is used to add custom instructions to the constraint system. -func (builder *builder) AddInstruction(bID constraint.BlueprintID, calldata []uint32) []uint32 { +func (builder *builder[E]) AddInstruction(bID constraint.BlueprintID, calldata []uint32) []uint32 { return builder.cs.AddInstruction(bID, calldata) } // AddBlueprint adds a custom blueprint to the constraint system. -func (builder *builder) AddBlueprint(b constraint.Blueprint) constraint.BlueprintID { +func (builder *builder[E]) AddBlueprint(b constraint.Blueprint) constraint.BlueprintID { return builder.cs.AddBlueprint(b) } -func (builder *builder) InternalVariable(wireID uint32) frontend.Variable { +func (builder *builder[E]) InternalVariable(wireID uint32) frontend.Variable { return expr.NewLinearExpression(int(wireID), builder.tOne) } // ToCanonicalVariable converts a frontend.Variable to a constraint system specific Variable // ! Experimental: use in conjunction with constraint.CustomizableSystem -func (builder *builder) ToCanonicalVariable(in frontend.Variable) frontend.CanonicalVariable { - if t, ok := in.(expr.LinearExpression); ok { +func (builder *builder[E]) ToCanonicalVariable(in frontend.Variable) frontend.CanonicalVariable { + if t, ok := in.(expr.LinearExpression[E]); ok { assertIsSet(t) return builder.getLinearExpression(t) } else { diff --git a/frontend/cs/r1cs/r1cs_test.go b/frontend/cs/r1cs/r1cs_test.go index bac8179e..6c208b7a 100644 --- a/frontend/cs/r1cs/r1cs_test.go +++ b/frontend/cs/r1cs/r1cs_test.go @@ -10,13 +10,13 @@ import ( "time" "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/internal/expr" ) -func TestQuickSort(t *testing.T) { - - toSort := make(expr.LinearExpression, 12) +func testQuickSortParametric[E constraint.Element](t *testing.T) { + toSort := make(expr.LinearExpression[E], 12) rand := 3 for i := 0; i < 12; i++ { toSort[i].VID = rand @@ -33,12 +33,16 @@ func TestQuickSort(t *testing.T) { t.Fatal("err sorting linear expression") } } +} +func TestQuickSort(t *testing.T) { + testQuickSortParametric[constraint.U64](t) + testQuickSortParametric[constraint.U32](t) } func TestReduce(t *testing.T) { - cs := newBuilder(ecc.BN254.ScalarField(), frontend.CompileConfig{}) + cs := newBuilder[constraint.U64](ecc.BN254.ScalarField(), frontend.CompileConfig{}) x := cs.newInternalVariable() y := cs.newInternalVariable() z := cs.newInternalVariable() @@ -50,7 +54,7 @@ func TestReduce(t *testing.T) { e := cs.Mul(z, 2) f := cs.Mul(z, 2) - toTest := (cs.Add(a, b, c, d, e, f)).(expr.LinearExpression) + toTest := (cs.Add(a, b, c, d, e, f)).(expr.LinearExpression[constraint.U64]) // check sizes if len(toTest) != 3 { @@ -60,7 +64,7 @@ func TestReduce(t *testing.T) { } func TestCompress(t *testing.T) { - cs := newBuilder(ecc.BN254.ScalarField(), frontend.CompileConfig{CompressThreshold: 3}) + cs := newBuilder[constraint.U64](ecc.BN254.ScalarField(), frontend.CompileConfig{CompressThreshold: 3}) vars := make([]frontend.Variable, 4) for i := range vars { v := cs.newInternalVariable() @@ -69,18 +73,18 @@ func TestCompress(t *testing.T) { // if add two variables, then should not compress v1 := cs.Add(vars[0], vars[1]) - if vli1 := v1.(expr.LinearExpression); len(vli1) != 2 { + if vli1 := v1.(expr.LinearExpression[constraint.U64]); len(vli1) != 2 { t.Fatalf("expected linear expression length 2, got %d", len(vli1)) } // if add three vars, then should compress v2 := cs.Add(vars[0], vars[1], vars[2]) - if vli2 := v2.(expr.LinearExpression); len(vli2) != 1 { + if vli2 := v2.(expr.LinearExpression[constraint.U64]); len(vli2) != 1 { t.Fatalf("expected linear expression length 1, got %d", len(vli2)) } } func BenchmarkReduce(b *testing.B) { - cs := newBuilder(ecc.BN254.ScalarField(), frontend.CompileConfig{}) + cs := newBuilder[constraint.U64](ecc.BN254.ScalarField(), frontend.CompileConfig{}) // 4 interesting cases; // Add many small linear expressions // Add few large linear expressions diff --git a/frontend/cs/scs/api.go b/frontend/cs/scs/api.go index ae919d67..a3abeaa2 100644 --- a/frontend/cs/scs/api.go +++ b/frontend/cs/scs/api.go @@ -19,10 +19,12 @@ import ( "github.com/consensys/gnark/frontend/internal/expr" "github.com/consensys/gnark/frontend/schema" "github.com/consensys/gnark/internal/frontendtype" + "github.com/consensys/gnark/internal/gkr/gkrinfo" + "github.com/consensys/gnark/internal/smallfields" "github.com/consensys/gnark/std/math/bits" ) -func (builder *builder) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { +func (builder *builder[E]) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { // separate the constant part from the variables vars, k := builder.filterConstantSum(append([]frontend.Variable{i1, i2}, in...)) @@ -39,7 +41,7 @@ func (builder *builder) Add(i1, i2 frontend.Variable, in ...frontend.Variable) f return builder.splitSum(vars[0], vars[1:], &k) } -func (builder *builder) MulAcc(a, b, c frontend.Variable) frontend.Variable { +func (builder *builder[E]) MulAcc(a, b, c frontend.Variable) frontend.Variable { if fastTrack := builder.mulAccFastTrack(a, b, c); fastTrack != nil { return fastTrack @@ -53,18 +55,18 @@ func (builder *builder) MulAcc(a, b, c frontend.Variable) frontend.Variable { // let a = a' * α, b = b' * β, c = c' * α // then a + b * c = a' * α + (b' * c') (β * α) // thus qL = a', qR = 0, qM = b'c' -func (builder *builder) mulAccFastTrack(a, b, c frontend.Variable) frontend.Variable { +func (builder *builder[E]) mulAccFastTrack(a, b, c frontend.Variable) frontend.Variable { var ( - aVar, bVar, cVar expr.Term + aVar, bVar, cVar expr.Term[E] ok bool ) - if aVar, ok = a.(expr.Term); !ok { + if aVar, ok = a.(expr.Term[E]); !ok { return nil } - if bVar, ok = b.(expr.Term); !ok { + if bVar, ok = b.(expr.Term[E]); !ok { return nil } - if cVar, ok = c.(expr.Term); !ok { + if cVar, ok = c.(expr.Term[E]); !ok { return nil } @@ -77,21 +79,22 @@ func (builder *builder) mulAccFastTrack(a, b, c frontend.Variable) frontend.Vari } res := builder.newInternalVariable() - builder.addPlonkConstraint(sparseR1C{ + var zero E + builder.addPlonkConstraint(sparseR1C[E]{ xa: aVar.VID, xb: bVar.VID, xc: res.VID, qL: aVar.Coeff, - qR: constraint.Element{}, + qR: zero, qO: builder.tMinusOne, qM: builder.cs.Mul(bVar.Coeff, cVar.Coeff), - qC: constraint.Element{}, + qC: zero, commitment: 0, }) return res } -func (builder *builder) neg(in []frontend.Variable) []frontend.Variable { +func (builder *builder[E]) neg(in []frontend.Variable) []frontend.Variable { res := make([]frontend.Variable, len(in)) for i := 0; i < len(in); i++ { @@ -100,22 +103,22 @@ func (builder *builder) neg(in []frontend.Variable) []frontend.Variable { return res } -func (builder *builder) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { +func (builder *builder[E]) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { r := builder.neg(append([]frontend.Variable{i2}, in...)) return builder.Add(i1, r[0], r[1:]...) } -func (builder *builder) Neg(i1 frontend.Variable) frontend.Variable { +func (builder *builder[E]) Neg(i1 frontend.Variable) frontend.Variable { if n, ok := builder.constantValue(i1); ok { n = builder.cs.Neg(n) return builder.cs.ToBigInt(n) } - v := i1.(expr.Term) + v := i1.(expr.Term[E]) v.Coeff = builder.cs.Neg(v.Coeff) return v } -func (builder *builder) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { +func (builder *builder[E]) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { vars, k := builder.filterConstantProd(append([]frontend.Variable{i1, i2}, in...)) if len(vars) == 0 { return builder.cs.ToBigInt(k) @@ -134,12 +137,12 @@ func (builder *builder) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) f } // returns t*m -func (builder *builder) mulConstant(t expr.Term, m constraint.Element) expr.Term { +func (builder *builder[E]) mulConstant(t expr.Term[E], m E) expr.Term[E] { t.Coeff = builder.cs.Mul(t.Coeff, m) return t } -func (builder *builder) DivUnchecked(i1, i2 frontend.Variable) frontend.Variable { +func (builder *builder[E]) DivUnchecked(i1, i2 frontend.Variable) frontend.Variable { c1, i1Constant := builder.constantValue(i1) c2, i2Constant := builder.constantValue(i2) @@ -156,34 +159,34 @@ func (builder *builder) DivUnchecked(i1, i2 frontend.Variable) frontend.Variable panic("inverse by constant(0)") } c2, _ = builder.cs.Inverse(c2) - return builder.mulConstant(i1.(expr.Term), c2) + return builder.mulConstant(i1.(expr.Term[E]), c2) } if i1Constant { res := builder.Inverse(i2) - return builder.mulConstant(res.(expr.Term), c1) + return builder.mulConstant(res.(expr.Term[E]), c1) } // res * i2 == i1 res := builder.newInternalVariable() - builder.addPlonkConstraint(sparseR1C{ + builder.addPlonkConstraint(sparseR1C[E]{ xa: res.VID, - xb: i2.(expr.Term).VID, - xc: i1.(expr.Term).VID, - qM: i2.(expr.Term).Coeff, - qO: builder.cs.Neg(i1.(expr.Term).Coeff), + xb: i2.(expr.Term[E]).VID, + xc: i1.(expr.Term[E]).VID, + qM: i2.(expr.Term[E]).Coeff, + qO: builder.cs.Neg(i1.(expr.Term[E]).Coeff), }) return res } -func (builder *builder) Div(i1, i2 frontend.Variable) frontend.Variable { +func (builder *builder[E]) Div(i1, i2 frontend.Variable) frontend.Variable { // note that here we ensure that v2 can't be 0, but it costs us one extra constraint builder.Inverse(i2) return builder.DivUnchecked(i1, i2) } -func (builder *builder) Inverse(i1 frontend.Variable) frontend.Variable { +func (builder *builder[E]) Inverse(i1 frontend.Variable) frontend.Variable { if c, ok := builder.constantValue(i1); ok { if c.IsZero() { panic("inverse by constant(0)") @@ -191,11 +194,11 @@ func (builder *builder) Inverse(i1 frontend.Variable) frontend.Variable { c, _ = builder.cs.Inverse(c) return builder.cs.ToBigInt(c) } - t := i1.(expr.Term) + t := i1.(expr.Term[E]) res := builder.newInternalVariable() // res * i1 - 1 == 0 - constraint := sparseR1C{ + constraint := sparseR1C[E]{ xa: res.VID, xb: t.VID, qM: t.Coeff, @@ -215,7 +218,7 @@ func (builder *builder) Inverse(i1 frontend.Variable) frontend.Variable { // --------------------------------------------------------------------------------------------- // Bit operations -func (builder *builder) ToBinary(i1 frontend.Variable, n ...int) []frontend.Variable { +func (builder *builder[E]) ToBinary(i1 frontend.Variable, n ...int) []frontend.Variable { // nbBits nbBits := builder.cs.FieldBitLen() if len(n) == 1 { @@ -228,11 +231,11 @@ func (builder *builder) ToBinary(i1 frontend.Variable, n ...int) []frontend.Vari return bits.ToBinary(builder, i1, bits.WithNbDigits(nbBits)) } -func (builder *builder) FromBinary(b ...frontend.Variable) frontend.Variable { +func (builder *builder[E]) FromBinary(b ...frontend.Variable) frontend.Variable { return bits.FromBinary(builder, b) } -func (builder *builder) Xor(a, b frontend.Variable) frontend.Variable { +func (builder *builder[E]) Xor(a, b frontend.Variable) frontend.Variable { // pre condition: a, b must be booleans builder.AssertIsBoolean(a) builder.AssertIsBoolean(b) @@ -263,7 +266,7 @@ func (builder *builder) Xor(a, b frontend.Variable) frontend.Variable { _b = _a } if bConstant { - xa := a.(expr.Term) + xa := a.(expr.Term[E]) // 1 - 2b qL := builder.tOne qL = builder.cs.Sub(qL, _b) @@ -271,7 +274,7 @@ func (builder *builder) Xor(a, b frontend.Variable) frontend.Variable { qL = builder.cs.Mul(qL, xa.Coeff) // (1-2b)a + b == res - builder.addPlonkConstraint(sparseR1C{ + builder.addPlonkConstraint(sparseR1C[E]{ xa: xa.VID, xc: res.VID, qL: qL, @@ -281,8 +284,8 @@ func (builder *builder) Xor(a, b frontend.Variable) frontend.Variable { // builder.addPlonkConstraint(xa, xb, res, builder.st.CoeffID(oneMinusTwoB), constraint.CoeffIdZero, constraint.CoeffIdZero, constraint.CoeffIdZero, constraint.CoeffIdMinusOne, builder.st.CoeffID(_b)) return res } - xa := a.(expr.Term) - xb := b.(expr.Term) + xa := a.(expr.Term[E]) + xb := b.(expr.Term[E]) // -a - b + 2ab + res == 0 qM := builder.tOne @@ -293,7 +296,7 @@ func (builder *builder) Xor(a, b frontend.Variable) frontend.Variable { qL := builder.cs.Neg(xa.Coeff) qR := builder.cs.Neg(xb.Coeff) - builder.addPlonkConstraint(sparseR1C{ + builder.addPlonkConstraint(sparseR1C[E]{ xa: xa.VID, xb: xb.VID, xc: res.VID, @@ -306,7 +309,7 @@ func (builder *builder) Xor(a, b frontend.Variable) frontend.Variable { return res } -func (builder *builder) Or(a, b frontend.Variable) frontend.Variable { +func (builder *builder[E]) Or(a, b frontend.Variable) frontend.Variable { builder.AssertIsBoolean(a) builder.AssertIsBoolean(b) @@ -336,8 +339,8 @@ func (builder *builder) Or(a, b frontend.Variable) frontend.Variable { } res := builder.newInternalVariable() builder.MarkBoolean(res) - xa := a.(expr.Term) - xb := b.(expr.Term) + xa := a.(expr.Term[E]) + xb := b.(expr.Term[E]) // -a - b + ab + res == 0 qM := builder.cs.Mul(xa.Coeff, xb.Coeff) @@ -345,7 +348,7 @@ func (builder *builder) Or(a, b frontend.Variable) frontend.Variable { qL := builder.cs.Neg(xa.Coeff) qR := builder.cs.Neg(xb.Coeff) - builder.addPlonkConstraint(sparseR1C{ + builder.addPlonkConstraint(sparseR1C[E]{ xa: xa.VID, xb: xb.VID, xc: res.VID, @@ -357,7 +360,7 @@ func (builder *builder) Or(a, b frontend.Variable) frontend.Variable { return res } -func (builder *builder) And(a, b frontend.Variable) frontend.Variable { +func (builder *builder[E]) And(a, b frontend.Variable) frontend.Variable { builder.AssertIsBoolean(a) builder.AssertIsBoolean(b) res := builder.Mul(a, b) @@ -368,7 +371,7 @@ func (builder *builder) And(a, b frontend.Variable) frontend.Variable { // --------------------------------------------------------------------------------------------- // Conditionals -func (builder *builder) Select(b frontend.Variable, i1, i2 frontend.Variable) frontend.Variable { +func (builder *builder[E]) Select(b frontend.Variable, i1, i2 frontend.Variable) frontend.Variable { _b, bConstant := builder.constantValue(b) if bConstant { @@ -390,7 +393,7 @@ func (builder *builder) Select(b frontend.Variable, i1, i2 frontend.Variable) fr return builder.Add(l, i2) } -func (builder *builder) Lookup2(b0, b1 frontend.Variable, i0, i1, i2, i3 frontend.Variable) frontend.Variable { +func (builder *builder[E]) Lookup2(b0, b1 frontend.Variable, i0, i1, i2, i3 frontend.Variable) frontend.Variable { // ensure that bits are actually bits. Adds no constraints if the variables // are already constrained. builder.AssertIsBoolean(b0) @@ -436,7 +439,7 @@ func (builder *builder) Lookup2(b0, b1 frontend.Variable, i0, i1, i2, i3 fronten } -func (builder *builder) IsZero(i1 frontend.Variable) frontend.Variable { +func (builder *builder[E]) IsZero(i1 frontend.Variable) frontend.Variable { if a, ok := builder.constantValue(i1); ok { if a.IsZero() { return 1 @@ -447,7 +450,7 @@ func (builder *builder) IsZero(i1 frontend.Variable) frontend.Variable { // x = 1/a // in a hint (x == 0 if a == 0) // m = -a*x + 1 // constrain m to be 1 if a == 0 // a * m = 0 // constrain m to be 0 if a != 0 - a := i1.(expr.Term) + a := i1.(expr.Term[E]) m := builder.newInternalVariable() // x = 1/a // in a hint (x == 0 if a == 0) @@ -459,8 +462,8 @@ func (builder *builder) IsZero(i1 frontend.Variable) frontend.Variable { // m = -a*x + 1 // constrain m to be 1 if a == 0 // a*x + m - 1 == 0 - X := x[0].(expr.Term) - builder.addPlonkConstraint(sparseR1C{ + X := x[0].(expr.Term[E]) + builder.addPlonkConstraint(sparseR1C[E]{ xa: a.VID, xb: X.VID, xc: m.VID, @@ -470,7 +473,7 @@ func (builder *builder) IsZero(i1 frontend.Variable) frontend.Variable { }) // a * m = 0 // constrain m to be 0 if a != 0 - builder.addPlonkConstraint(sparseR1C{ + builder.addPlonkConstraint(sparseR1C[E]{ xa: a.VID, xb: m.VID, qM: a.Coeff, @@ -481,7 +484,7 @@ func (builder *builder) IsZero(i1 frontend.Variable) frontend.Variable { return m } -func (builder *builder) Cmp(i1, i2 frontend.Variable) frontend.Variable { +func (builder *builder[E]) Cmp(i1, i2 frontend.Variable) frontend.Variable { nbBits := builder.cs.FieldBitLen() // in AssertIsLessOrEq we omitted comparison against modulus for the left @@ -508,7 +511,7 @@ func (builder *builder) Cmp(i1, i2 frontend.Variable) frontend.Variable { return res } -func (builder *builder) Println(a ...frontend.Variable) { +func (builder *builder[E]) Println(a ...frontend.Variable) { var log constraint.LogEntry // prefix log line with file.go:line @@ -522,7 +525,7 @@ func (builder *builder) Println(a ...frontend.Variable) { if i > 0 { sbb.WriteByte(' ') } - if v, ok := arg.(expr.Term); ok { + if v, ok := arg.(expr.Term[E]); ok { sbb.WriteString("%s") // we set limits to the linear expression, so that the log printer @@ -539,9 +542,9 @@ func (builder *builder) Println(a ...frontend.Variable) { builder.cs.AddLog(log) } -func (builder *builder) printArg(log *constraint.LogEntry, sbb *strings.Builder, a frontend.Variable) { +func (builder *builder[E]) printArg(log *constraint.LogEntry, sbb *strings.Builder, a frontend.Variable) { - leafCount, err := schema.Walk(a, tVariable, nil) + leafCount, err := schema.Walk(builder.Field(), a, tVariable, nil) count := leafCount.Public + leafCount.Secret // no variables in nested struct, we use fmt std print function @@ -560,34 +563,37 @@ func (builder *builder) printArg(log *constraint.LogEntry, sbb *strings.Builder, sbb.WriteString(", ") } - v := tValue.Interface().(expr.Term) + v := tValue.Interface().(expr.Term[E]) // we set limits to the linear expression, so that the log printer // can evaluate it before printing it log.ToResolve = append(log.ToResolve, constraint.LinearExpression{builder.cs.MakeTerm(v.Coeff, v.VID)}) return nil } // ignoring error, printer() doesn't return errors - _, _ = schema.Walk(a, tVariable, printer) + _, _ = schema.Walk(builder.Field(), a, tVariable, printer) sbb.WriteByte('}') } -func (builder *builder) Compiler() frontend.Compiler { +func (builder *builder[E]) Compiler() frontend.Compiler { return builder } -func (builder *builder) Commit(v ...frontend.Variable) (frontend.Variable, error) { +func (builder *builder[E]) Commit(v ...frontend.Variable) (frontend.Variable, error) { + if smallfields.IsSmallField(builder.Field()) { + return nil, fmt.Errorf("commitment not supported for small field %s", builder.Field()) + } commitments := builder.cs.GetCommitments().(constraint.PlonkCommitments) - v = filterConstants(v) // TODO: @Tabaie Settle on a way to represent even constants; conventional hash? + v = filterConstants[E](v) // TODO: @Tabaie Settle on a way to represent even constants; conventional hash? committed := make([]int, len(v)) for i, vI := range v { // TODO @Tabaie Perf; If public, just hash it - vINeg := builder.Neg(vI).(expr.Term) + vINeg := builder.Neg(vI).(expr.Term[E]) committed[i] = builder.cs.GetNbConstraints() // a constraint to enforce consistency between the commitment and committed value // - v + comm(n) = 0 - builder.addPlonkConstraint(sparseR1C{xa: vINeg.VID, qL: vINeg.Coeff, commitment: constraint.COMMITTED}) + builder.addPlonkConstraint(sparseR1C[E]{xa: vINeg.VID, qL: vINeg.Coeff, commitment: constraint.COMMITTED}) } inputs := make([]frontend.Variable, len(v)+1) @@ -598,10 +604,10 @@ func (builder *builder) Commit(v ...frontend.Variable) (frontend.Variable, error return nil, err } - commitmentVar := builder.Neg(outs[0]).(expr.Term) + commitmentVar := builder.Neg(outs[0]).(expr.Term[E]) commitmentConstraintIndex := builder.cs.GetNbConstraints() // RHS will be provided by both prover and verifier independently, as for a public wire - builder.addPlonkConstraint(sparseR1C{xa: commitmentVar.VID, qL: commitmentVar.Coeff, commitment: constraint.COMMITMENT}) // value will be injected later + builder.addPlonkConstraint(sparseR1C[E]{xa: commitmentVar.VID, qL: commitmentVar.Coeff, commitment: constraint.COMMITMENT}) // value will be injected later return outs[0], builder.cs.AddCommitment(constraint.PlonkCommitment{ CommitmentIndex: commitmentConstraintIndex, @@ -610,7 +616,7 @@ func (builder *builder) Commit(v ...frontend.Variable) (frontend.Variable, error } // EvaluatePlonkExpression in the form of res = qL.a + qR.b + qM.ab + qC -func (builder *builder) EvaluatePlonkExpression(a, b frontend.Variable, qL, qR, qM, qC int) frontend.Variable { +func (builder *builder[E]) EvaluatePlonkExpression(a, b frontend.Variable, qL, qR, qM, qC int) frontend.Variable { _, aConstant := builder.constantValue(a) _, bConstant := builder.constantValue(b) if aConstant || bConstant { @@ -623,21 +629,21 @@ func (builder *builder) EvaluatePlonkExpression(a, b frontend.Variable, qL, qR, } res := builder.newInternalVariable() - builder.addPlonkConstraint(sparseR1C{ - xa: a.(expr.Term).VID, - xb: b.(expr.Term).VID, + builder.addPlonkConstraint(sparseR1C[E]{ + xa: a.(expr.Term[E]).VID, + xb: b.(expr.Term[E]).VID, xc: res.VID, - qL: builder.cs.Mul(builder.cs.FromInterface(qL), a.(expr.Term).Coeff), - qR: builder.cs.Mul(builder.cs.FromInterface(qR), b.(expr.Term).Coeff), + qL: builder.cs.Mul(builder.cs.FromInterface(qL), a.(expr.Term[E]).Coeff), + qR: builder.cs.Mul(builder.cs.FromInterface(qR), b.(expr.Term[E]).Coeff), qO: builder.tMinusOne, - qM: builder.cs.Mul(builder.cs.FromInterface(qM), builder.cs.Mul(a.(expr.Term).Coeff, b.(expr.Term).Coeff)), + qM: builder.cs.Mul(builder.cs.FromInterface(qM), builder.cs.Mul(a.(expr.Term[E]).Coeff, b.(expr.Term[E]).Coeff)), qC: builder.cs.FromInterface(qC), }) return res } // AddPlonkConstraint asserts qL.a + qR.b + qO.o + qM.ab + qC = 0 -func (builder *builder) AddPlonkConstraint(a, b, o frontend.Variable, qL, qR, qO, qM, qC int) { +func (builder *builder[E]) AddPlonkConstraint(a, b, o frontend.Variable, qL, qR, qO, qM, qC int) { _, aConstant := builder.constantValue(a) _, bConstant := builder.constantValue(b) _, oConstant := builder.constantValue(o) @@ -655,32 +661,32 @@ func (builder *builder) AddPlonkConstraint(a, b, o frontend.Variable, qL, qR, qO return } - builder.addPlonkConstraint(sparseR1C{ - xa: a.(expr.Term).VID, - xb: b.(expr.Term).VID, - xc: o.(expr.Term).VID, - qL: builder.cs.Mul(builder.cs.FromInterface(qL), a.(expr.Term).Coeff), - qR: builder.cs.Mul(builder.cs.FromInterface(qR), b.(expr.Term).Coeff), - qO: builder.cs.Mul(builder.cs.FromInterface(qO), o.(expr.Term).Coeff), - qM: builder.cs.Mul(builder.cs.FromInterface(qM), builder.cs.Mul(a.(expr.Term).Coeff, b.(expr.Term).Coeff)), + builder.addPlonkConstraint(sparseR1C[E]{ + xa: a.(expr.Term[E]).VID, + xb: b.(expr.Term[E]).VID, + xc: o.(expr.Term[E]).VID, + qL: builder.cs.Mul(builder.cs.FromInterface(qL), a.(expr.Term[E]).Coeff), + qR: builder.cs.Mul(builder.cs.FromInterface(qR), b.(expr.Term[E]).Coeff), + qO: builder.cs.Mul(builder.cs.FromInterface(qO), o.(expr.Term[E]).Coeff), + qM: builder.cs.Mul(builder.cs.FromInterface(qM), builder.cs.Mul(a.(expr.Term[E]).Coeff, b.(expr.Term[E]).Coeff)), qC: builder.cs.FromInterface(qC), }) } -func filterConstants(v []frontend.Variable) []frontend.Variable { +func filterConstants[E constraint.Element](v []frontend.Variable) []frontend.Variable { res := make([]frontend.Variable, 0, len(v)) for _, vI := range v { - if _, ok := vI.(expr.Term); ok { + if _, ok := vI.(expr.Term[E]); ok { res = append(res, vI) } } return res } -func (*builder) FrontendType() frontendtype.Type { +func (*builder[E]) FrontendType() frontendtype.Type { return frontendtype.SCS } -func (builder *builder) SetGkrInfo(info constraint.GkrInfo) error { +func (builder *builder[E]) SetGkrInfo(info gkrinfo.StoringInfo) error { return builder.cs.AddGkr(info) } diff --git a/frontend/cs/scs/api_assertions.go b/frontend/cs/scs/api_assertions.go index 2c6bd3a2..3afc5acc 100644 --- a/frontend/cs/scs/api_assertions.go +++ b/frontend/cs/scs/api_assertions.go @@ -15,7 +15,7 @@ import ( ) // AssertIsEqual fails if i1 != i2 -func (builder *builder) AssertIsEqual(i1, i2 frontend.Variable) { +func (builder *builder[E]) AssertIsEqual(i1, i2 frontend.Variable) { c1, i1Constant := builder.constantValue(i1) c2, i2Constant := builder.constantValue(i2) @@ -32,11 +32,11 @@ func (builder *builder) AssertIsEqual(i1, i2 frontend.Variable) { c2 = c1 } if i2Constant { - xa := i1.(expr.Term) + xa := i1.(expr.Term[E]) c2 := builder.cs.Neg(c2) // xa - i2 == 0 - toAdd := sparseR1C{ + toAdd := sparseR1C[E]{ xa: xa.VID, qL: xa.Coeff, qC: c2, @@ -50,12 +50,12 @@ func (builder *builder) AssertIsEqual(i1, i2 frontend.Variable) { } return } - xa := i1.(expr.Term) - xb := i2.(expr.Term) + xa := i1.(expr.Term[E]) + xb := i2.(expr.Term[E]) xb.Coeff = builder.cs.Neg(xb.Coeff) // xa - xb == 0 - toAdd := sparseR1C{ + toAdd := sparseR1C[E]{ xa: xa.VID, xb: xb.VID, qL: xa.Coeff, @@ -72,20 +72,20 @@ func (builder *builder) AssertIsEqual(i1, i2 frontend.Variable) { } // AssertIsDifferent fails if i1 == i2 -func (builder *builder) AssertIsDifferent(i1, i2 frontend.Variable) { +func (builder *builder[E]) AssertIsDifferent(i1, i2 frontend.Variable) { s := builder.Sub(i1, i2) if c, ok := builder.constantValue(s); ok { if c.IsZero() { panic("AssertIsDifferent(x,x) will never be satisfied") } - } else if t := s.(expr.Term); t.Coeff.IsZero() { + } else if t := s.(expr.Term[E]); t.Coeff.IsZero() { panic("AssertIsDifferent(x,x) will never be satisfied") } builder.Inverse(s) } // AssertIsBoolean fails if v != 0 ∥ v != 1 -func (builder *builder) AssertIsBoolean(i1 frontend.Variable) { +func (builder *builder[E]) AssertIsBoolean(i1 frontend.Variable) { if c, ok := builder.constantValue(i1); ok { if !(c.IsZero() || builder.cs.IsOne(c)) { panic(fmt.Sprintf("assertIsBoolean failed: constant(%s)", builder.cs.String(c))) @@ -93,7 +93,7 @@ func (builder *builder) AssertIsBoolean(i1 frontend.Variable) { return } - v := i1.(expr.Term) + v := i1.(expr.Term[E]) if builder.IsBoolean(v) { return } @@ -104,7 +104,7 @@ func (builder *builder) AssertIsBoolean(i1 frontend.Variable) { // qM = -v.Coeff*v.Coeff qM := builder.cs.Neg(v.Coeff) qM = builder.cs.Mul(qM, v.Coeff) - toAdd := sparseR1C{ + toAdd := sparseR1C[E]{ xa: v.VID, qL: v.Coeff, qM: qM, @@ -118,7 +118,7 @@ func (builder *builder) AssertIsBoolean(i1 frontend.Variable) { } -func (builder *builder) AssertIsCrumb(i1 frontend.Variable) { +func (builder *builder[E]) AssertIsCrumb(i1 frontend.Variable) { const errorMsg = "AssertIsCrumb: input is not a crumb" if c, ok := builder.constantValue(i1); ok { if i, ok := builder.cs.Uint64(c); ok && i < 4 { @@ -130,11 +130,11 @@ func (builder *builder) AssertIsCrumb(i1 frontend.Variable) { // i1 (i1-1) (i1-2) (i1-3) = (i1² - 3i1) (i1² - 3i1 + 2) // take X := i1² - 3i1 and we get X (X+2) = 0 - x := builder.MulAcc(builder.Mul(-3, i1), i1, i1).(expr.Term) + x := builder.MulAcc(builder.Mul(-3, i1), i1, i1).(expr.Term[E]) // TODO @Tabaie Ideally this entire function would live in std/math/bits as it is quite specialized; // however using two generic MulAccs and an AssertIsEqual results in three constraints rather than two. - builder.addPlonkConstraint(sparseR1C{ + builder.addPlonkConstraint(sparseR1C[E]{ xa: x.VID, xb: x.VID, qL: builder.cs.FromInterface(2), @@ -143,34 +143,33 @@ func (builder *builder) AssertIsCrumb(i1 frontend.Variable) { } // AssertIsLessOrEqual fails if v > bound -func (builder *builder) AssertIsLessOrEqual(v frontend.Variable, bound frontend.Variable) { +func (builder *builder[E]) AssertIsLessOrEqual(v frontend.Variable, bound frontend.Variable) { cv, vConst := builder.constantValue(v) cb, bConst := builder.constantValue(bound) - // both inputs are constants - if vConst && bConst { + switch { + case vConst && bConst: // both inputs are constants bv, bb := builder.cs.ToBigInt(cv), builder.cs.ToBigInt(cb) if bv.Cmp(bb) == 1 { panic(fmt.Sprintf("AssertIsLessOrEqual: %s > %s", bv.String(), bb.String())) } - } - - // bound is constant - if bConst { + return + case bConst: // bound is constant nbBits := builder.cs.FieldBitLen() vBits := bits.ToBinary(builder, v, bits.WithNbDigits(nbBits), bits.WithUnconstrainedOutputs()) builder.MustBeLessOrEqCst(vBits, builder.cs.ToBigInt(cb), v) return + default: + if b, ok := bound.(expr.Term[E]); ok { + builder.mustBeLessOrEqVar(v, b) + } else { + panic(fmt.Sprintf("expected bound type expr.Term, got %T", bound)) + } } - if b, ok := bound.(expr.Term); ok { - builder.mustBeLessOrEqVar(v, b) - } else { - panic(fmt.Sprintf("expected bound type expr.Term, got %T", bound)) - } } -func (builder *builder) mustBeLessOrEqVar(a frontend.Variable, bound expr.Term) { +func (builder *builder[E]) mustBeLessOrEqVar(a frontend.Variable, bound expr.Term[E]) { var debugInfo []constraint.DebugInfo if debug.Debug { debugInfo = []constraint.DebugInfo{builder.newDebugInfo("mustBeLessOrEq", a, " <= ", bound)} @@ -198,7 +197,7 @@ func (builder *builder) mustBeLessOrEqVar(a frontend.Variable, bound expr.Term) t := builder.Select(boundBits[i], 0, p[i+1]) // (1 - t - ai) * ai == 0 - l := builder.Sub(1, t, aBits[i]).(expr.Term) + l := builder.Sub(1, t, aBits[i]).(expr.Term[E]) // note if bound[i] == 1, this constraint is (1 - ai) * ai == 0 // → this is a boolean constraint @@ -207,15 +206,15 @@ func (builder *builder) mustBeLessOrEqVar(a frontend.Variable, bound expr.Term) if ai, ok := builder.constantValue(aBits[i]); ok { // a is constant; ensure l == 0 l.Coeff = builder.cs.Mul(l.Coeff, ai) - builder.addPlonkConstraint(sparseR1C{ + builder.addPlonkConstraint(sparseR1C[E]{ xa: l.VID, qL: l.Coeff, }, debugInfo...) } else { // l * a[i] == 0 - builder.addPlonkConstraint(sparseR1C{ + builder.addPlonkConstraint(sparseR1C[E]{ xa: l.VID, - xb: aBits[i].(expr.Term).VID, + xb: aBits[i].(expr.Term[E]).VID, qM: l.Coeff, }, debugInfo...) } @@ -227,7 +226,7 @@ func (builder *builder) mustBeLessOrEqVar(a frontend.Variable, bound expr.Term) // MustBeLessOrEqCst asserts that value represented using its bit decomposition // aBits is less or equal than constant bound. The method boolean constraints // the bits in aBits, so the caller can provide unconstrained bits. -func (builder *builder) MustBeLessOrEqCst(aBits []frontend.Variable, bound *big.Int, aForDebug frontend.Variable) { +func (builder *builder[E]) MustBeLessOrEqCst(aBits []frontend.Variable, bound *big.Int, aForDebug frontend.Variable) { nbBits := builder.cs.FieldBitLen() if len(aBits) > nbBits { @@ -276,12 +275,12 @@ func (builder *builder) MustBeLessOrEqCst(aBits []frontend.Variable, bound *big. if bound.Bit(i) == 0 { // (1 - p(i+1) - ai) * ai == 0 - l := builder.Sub(1, p[i+1], aBits[i]).(expr.Term) + l := builder.Sub(1, p[i+1], aBits[i]).(expr.Term[E]) //l = builder.Sub(l, ).(term) - builder.addPlonkConstraint(sparseR1C{ + builder.addPlonkConstraint(sparseR1C[E]{ xa: l.VID, - xb: aBits[i].(expr.Term).VID, + xb: aBits[i].(expr.Term[E]).VID, qM: builder.tOne, }, debugInfo...) } else { diff --git a/frontend/cs/scs/builder.go b/frontend/cs/scs/builder.go index 07f925e9..55e58b91 100644 --- a/frontend/cs/scs/builder.go +++ b/frontend/cs/scs/builder.go @@ -10,6 +10,8 @@ import ( "sort" "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/babybear" + "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/debug" "github.com/consensys/gnark/frontend" @@ -17,10 +19,11 @@ import ( "github.com/consensys/gnark/frontend/schema" "github.com/consensys/gnark/internal/circuitdefer" "github.com/consensys/gnark/internal/kvstore" - "github.com/consensys/gnark/internal/tinyfield" + "github.com/consensys/gnark/internal/smallfields/tinyfield" "github.com/consensys/gnark/internal/utils" "github.com/consensys/gnark/logger" + babybearr1cs "github.com/consensys/gnark/constraint/babybear" bls12377r1cs "github.com/consensys/gnark/constraint/bls12-377" bls12381r1cs "github.com/consensys/gnark/constraint/bls12-381" bls24315r1cs "github.com/consensys/gnark/constraint/bls24-315" @@ -28,21 +31,24 @@ import ( bn254r1cs "github.com/consensys/gnark/constraint/bn254" bw6633r1cs "github.com/consensys/gnark/constraint/bw6-633" bw6761r1cs "github.com/consensys/gnark/constraint/bw6-761" + koalabearr1cs "github.com/consensys/gnark/constraint/koalabear" "github.com/consensys/gnark/constraint/solver" tinyfieldr1cs "github.com/consensys/gnark/constraint/tinyfield" ) -func NewBuilder(field *big.Int, config frontend.CompileConfig) (frontend.Builder, error) { - return newBuilder(field, config), nil +// NewBuilder returns a new PLONKish/SparseR1CS builder which implements +// [frontend.API]. Additionally, this builder implements [frontend.Committer]. +func NewBuilder[E constraint.Element](field *big.Int, config frontend.CompileConfig) (frontend.Builder[E], error) { + return newBuilder[E](field, config), nil } -type builder struct { - cs constraint.SparseR1CS +type builder[E constraint.Element] struct { + cs constraint.SparseR1CS[E] config frontend.CompileConfig kvstore.Store // map for recording boolean constrained variables (to not constrain them twice) - mtBooleans map[expr.Term]struct{} + mtBooleans map[expr.Term[E]]struct{} // records multiplications constraint to avoid duplicates. // see mulConstraintExist(...) @@ -53,84 +59,102 @@ type builder struct { mAddInstructions map[uint64]int // frequently used coefficients - tOne, tMinusOne constraint.Element + tOne, tMinusOne E genericGate constraint.BlueprintID mulGate, addGate, boolGate constraint.BlueprintID // used to avoid repeated allocations - bufL expr.LinearExpression + bufL expr.LinearExpression[E] bufH []constraint.LinearExpression } // initialCapacity has quite some impact on frontend performance, especially on large circuits size // we may want to add build tags to tune that -func newBuilder(field *big.Int, config frontend.CompileConfig) *builder { - b := builder{ - mtBooleans: make(map[expr.Term]struct{}), +func newBuilder[E constraint.Element](field *big.Int, config frontend.CompileConfig) *builder[E] { + b := &builder[E]{ + mtBooleans: make(map[expr.Term[E]]struct{}), mMulInstructions: make(map[uint64]int, config.Capacity/2), mAddInstructions: make(map[uint64]int, config.Capacity/2), config: config, Store: kvstore.New(), - bufL: make(expr.LinearExpression, 20), + bufL: make(expr.LinearExpression[E], 20), } // init hint buffer. _ = b.hintBuffer(256) curve := utils.FieldToCurve(field) - switch curve { - case ecc.BLS12_377: - b.cs = bls12377r1cs.NewSparseR1CS(config.Capacity) - case ecc.BLS12_381: - b.cs = bls12381r1cs.NewSparseR1CS(config.Capacity) - case ecc.BN254: - b.cs = bn254r1cs.NewSparseR1CS(config.Capacity) - case ecc.BW6_761: - b.cs = bw6761r1cs.NewSparseR1CS(config.Capacity) - case ecc.BW6_633: - b.cs = bw6633r1cs.NewSparseR1CS(config.Capacity) - case ecc.BLS24_315: - b.cs = bls24315r1cs.NewSparseR1CS(config.Capacity) - case ecc.BLS24_317: - b.cs = bls24317r1cs.NewSparseR1CS(config.Capacity) - default: - if field.Cmp(tinyfield.Modulus()) == 0 { - b.cs = tinyfieldr1cs.NewSparseR1CS(config.Capacity) - break + switch bT := any(b).(type) { + case *builder[constraint.U64]: + switch curve { + case ecc.BLS12_377: + bT.cs = bls12377r1cs.NewSparseR1CS(config.Capacity) + case ecc.BLS12_381: + bT.cs = bls12381r1cs.NewSparseR1CS(config.Capacity) + case ecc.BN254: + bT.cs = bn254r1cs.NewSparseR1CS(config.Capacity) + case ecc.BW6_761: + bT.cs = bw6761r1cs.NewSparseR1CS(config.Capacity) + case ecc.BW6_633: + bT.cs = bw6633r1cs.NewSparseR1CS(config.Capacity) + case ecc.BLS24_315: + bT.cs = bls24315r1cs.NewSparseR1CS(config.Capacity) + case ecc.BLS24_317: + bT.cs = bls24317r1cs.NewSparseR1CS(config.Capacity) + default: + panic("not implemented") + } + case *builder[constraint.U32]: + switch curve { + default: + if field.Cmp(tinyfield.Modulus()) == 0 { + bT.cs = tinyfieldr1cs.NewSparseR1CS(config.Capacity) + break + } + if field.Cmp(babybear.Modulus()) == 0 { + bT.cs = babybearr1cs.NewSparseR1CS(config.Capacity) + break + } + if field.Cmp(koalabear.Modulus()) == 0 { + bT.cs = koalabearr1cs.NewSparseR1CS(config.Capacity) + break + } + panic("not implemented") } - panic("not implemented") + default: + panic("invalid constraint.Element type") } b.tOne = b.cs.One() b.tMinusOne = b.cs.FromInterface(-1) - b.genericGate = b.cs.AddBlueprint(&constraint.BlueprintGenericSparseR1C{}) - b.mulGate = b.cs.AddBlueprint(&constraint.BlueprintSparseR1CMul{}) - b.addGate = b.cs.AddBlueprint(&constraint.BlueprintSparseR1CAdd{}) - b.boolGate = b.cs.AddBlueprint(&constraint.BlueprintSparseR1CBool{}) + b.genericGate = b.cs.AddBlueprint(&constraint.BlueprintGenericSparseR1C[E]{}) + b.mulGate = b.cs.AddBlueprint(&constraint.BlueprintSparseR1CMul[E]{}) + b.addGate = b.cs.AddBlueprint(&constraint.BlueprintSparseR1CAdd[E]{}) + b.boolGate = b.cs.AddBlueprint(&constraint.BlueprintSparseR1CBool[E]{}) - return &b + return b } -func (builder *builder) Field() *big.Int { +func (builder *builder[E]) Field() *big.Int { return builder.cs.Field() } -func (builder *builder) FieldBitLen() int { +func (builder *builder[E]) FieldBitLen() int { return builder.cs.FieldBitLen() } // TODO @gbotrel doing a 2-step refactoring for now, frontend only. need to update constraint/SparseR1C. // qL⋅xa + qR⋅xb + qO⋅xc + qM⋅(xaxb) + qC == 0 -type sparseR1C struct { - xa, xb, xc int // wires - qL, qR, qO, qM, qC constraint.Element // coefficients +type sparseR1C[E constraint.Element] struct { + xa, xb, xc int // wires + qL, qR, qO, qM, qC E // coefficients commitment constraint.CommitmentConstraint } // a * b == c -func (builder *builder) addMulGate(a, b, c expr.Term) { +func (builder *builder[E]) addMulGate(a, b, c expr.Term[E]) { qM := builder.cs.Mul(a.Coeff, b.Coeff) QM := builder.cs.AddCoeff(qM) @@ -144,7 +168,7 @@ func (builder *builder) addMulGate(a, b, c expr.Term) { } // a + b + k == c -func (builder *builder) addAddGate(a, b expr.Term, xc uint32, k constraint.Element) { +func (builder *builder[E]) addAddGate(a, b expr.Term[E], xc uint32, k E) { qL := builder.cs.AddCoeff(a.Coeff) qR := builder.cs.AddCoeff(b.Coeff) qC := builder.cs.AddCoeff(k) @@ -160,7 +184,7 @@ func (builder *builder) addAddGate(a, b expr.Term, xc uint32, k constraint.Eleme }, builder.addGate) } -func (builder *builder) addBoolGate(c sparseR1C, debugInfo ...constraint.DebugInfo) { +func (builder *builder[E]) addBoolGate(c sparseR1C[E], debugInfo ...constraint.DebugInfo) { QL := builder.cs.AddCoeff(c.qL) QM := builder.cs.AddCoeff(c.qM) @@ -175,7 +199,7 @@ func (builder *builder) addBoolGate(c sparseR1C, debugInfo ...constraint.DebugIn } // addPlonkConstraint adds a sparseR1C to the underlying constraint system -func (builder *builder) addPlonkConstraint(c sparseR1C, debugInfo ...constraint.DebugInfo) { +func (builder *builder[E]) addPlonkConstraint(c sparseR1C[E], debugInfo ...constraint.DebugInfo) { if !c.qM.IsZero() && (c.xa == 0 || c.xb == 0) { // TODO this is internal but not easy to detect; if qM is set, but one or both of xa / xb is not, // since wireID == 0 is a valid wire, it may trigger unexpected behavior. @@ -211,19 +235,19 @@ func (builder *builder) addPlonkConstraint(c sparseR1C, debugInfo ...constraint. // newInternalVariable creates a new wire, appends it on the list of wires of the circuit, sets // the wire's id to the number of wires, and returns it -func (builder *builder) newInternalVariable() expr.Term { +func (builder *builder[E]) newInternalVariable() expr.Term[E] { idx := builder.cs.AddInternalVariable() return expr.NewTerm(idx, builder.tOne) } // PublicVariable creates a new Public Variable -func (builder *builder) PublicVariable(f schema.LeafInfo) frontend.Variable { +func (builder *builder[E]) PublicVariable(f schema.LeafInfo) frontend.Variable { idx := builder.cs.AddPublicVariable(f.FullName()) return expr.NewTerm(idx, builder.tOne) } // SecretVariable creates a new Secret Variable -func (builder *builder) SecretVariable(f schema.LeafInfo) frontend.Variable { +func (builder *builder[E]) SecretVariable(f schema.LeafInfo) frontend.Variable { idx := builder.cs.AddSecretVariable(f.FullName()) return expr.NewTerm(idx, builder.tOne) } @@ -232,7 +256,7 @@ func (builder *builder) SecretVariable(f schema.LeafInfo) frontend.Variable { // It factorizes Variable that appears multiple times with != coeff Ids // To ensure the determinism in the compile process, Variables are stored as public∥secret∥internal∥unset // for each visibility, the Variables are sorted from lowest ID to highest ID -func (builder *builder) reduce(l expr.LinearExpression) expr.LinearExpression { +func (builder *builder[E]) reduce(l expr.LinearExpression[E]) expr.LinearExpression[E] { // ensure our linear expression is sorted, by visibility and by Variable ID sort.Sort(l) @@ -251,25 +275,25 @@ func (builder *builder) reduce(l expr.LinearExpression) expr.LinearExpression { // IsBoolean returns true if given variable was marked as boolean in the compiler (see MarkBoolean) // Use with care; variable may not have been **constrained** to be boolean // This returns true if the v is a constant and v == 0 || v == 1. -func (builder *builder) IsBoolean(v frontend.Variable) bool { +func (builder *builder[E]) IsBoolean(v frontend.Variable) bool { if b, ok := builder.constantValue(v); ok { return (b.IsZero() || builder.cs.IsOne(b)) } - _, ok := builder.mtBooleans[v.(expr.Term)] + _, ok := builder.mtBooleans[v.(expr.Term[E])] return ok } // MarkBoolean sets (but do not constraint!) v to be boolean // This is useful in scenarios where a variable is known to be boolean through a constraint // that is not api.AssertIsBoolean. If v is a constant, this is a no-op. -func (builder *builder) MarkBoolean(v frontend.Variable) { +func (builder *builder[E]) MarkBoolean(v frontend.Variable) { if _, ok := builder.constantValue(v); ok { if !builder.IsBoolean(v) { panic("MarkBoolean called a non-boolean constant") } return } - builder.mtBooleans[v.(expr.Term)] = struct{}{} + builder.mtBooleans[v.(expr.Term[E])] = struct{}{} } var tVariable reflect.Type @@ -278,7 +302,7 @@ func init() { tVariable = reflect.ValueOf(struct{ A frontend.Variable }{}).FieldByName("A").Type() } -func (builder *builder) Compile() (constraint.ConstraintSystem, error) { +func (builder *builder[E]) Compile() (constraint.ConstraintSystemGeneric[E], error) { log := logger.Logger() log.Info(). Int("nbConstraints", builder.cs.GetNbConstraints()). @@ -297,7 +321,7 @@ func (builder *builder) Compile() (constraint.ConstraintSystem, error) { } // ConstantValue returns the big.Int value of v and true if v is a constant, false otherwise -func (builder *builder) ConstantValue(v frontend.Variable) (*big.Int, bool) { +func (builder *builder[E]) ConstantValue(v frontend.Variable) (*big.Int, bool) { coeff, ok := builder.constantValue(v) if !ok { return nil, false @@ -305,17 +329,18 @@ func (builder *builder) ConstantValue(v frontend.Variable) (*big.Int, bool) { return builder.cs.ToBigInt(coeff), true } -func (builder *builder) constantValue(v frontend.Variable) (constraint.Element, bool) { - if vv, ok := v.(expr.Term); ok { +func (builder *builder[E]) constantValue(v frontend.Variable) (E, bool) { + if vv, ok := v.(expr.Term[E]); ok { + var zero E if vv.Coeff.IsZero() { - return constraint.Element{}, true + return zero, true } - return constraint.Element{}, false + return zero, false } return builder.cs.FromInterface(v), true } -func (builder *builder) hintBuffer(size int) []constraint.LinearExpression { +func (builder *builder[E]) hintBuffer(size int) []constraint.LinearExpression { if cap(builder.bufH) < size { builder.bufH = make([]constraint.LinearExpression, 2*size) for i := 0; i < len(builder.bufH); i++ { @@ -338,17 +363,17 @@ func (builder *builder) hintBuffer(size int) []constraint.LinearExpression { // // No new constraints are added to the newly created wire and must be added // manually in the circuit. Failing to do so leads to solver failure. -func (builder *builder) NewHint(f solver.Hint, nbOutputs int, inputs ...frontend.Variable) ([]frontend.Variable, error) { +func (builder *builder[E]) NewHint(f solver.Hint, nbOutputs int, inputs ...frontend.Variable) ([]frontend.Variable, error) { return builder.newHint(f, solver.GetHintID(f), nbOutputs, inputs...) } -func (builder *builder) newHint(f solver.Hint, id solver.HintID, nbOutputs int, inputs ...frontend.Variable) ([]frontend.Variable, error) { +func (builder *builder[E]) newHint(f solver.Hint, id solver.HintID, nbOutputs int, inputs ...frontend.Variable) ([]frontend.Variable, error) { hintInputs := builder.hintBuffer(len(inputs)) // ensure inputs are set and pack them in a []uint64 for i, in := range inputs { switch t := in.(type) { - case expr.Term: + case expr.Term[E]: hintInputs[i][0] = builder.cs.MakeTerm(t.Coeff, t.VID) default: c := builder.cs.FromInterface(in) @@ -373,23 +398,23 @@ func (builder *builder) newHint(f solver.Hint, id solver.HintID, nbOutputs int, } // returns in split into a slice of compiledTerm and the sum of all constants in in as a bigInt -func (builder *builder) filterConstantSum(in []frontend.Variable) (expr.LinearExpression, constraint.Element) { - var res expr.LinearExpression +func (builder *builder[E]) filterConstantSum(in []frontend.Variable) (expr.LinearExpression[E], E) { + var res expr.LinearExpression[E] if len(in) <= cap(builder.bufL) { // we can use the temp buffer res = builder.bufL[:0] } else { - res = make(expr.LinearExpression, 0, len(in)) + res = make(expr.LinearExpression[E], 0, len(in)) } - b := constraint.Element{} + var b E for i := 0; i < len(in); i++ { if c, ok := builder.constantValue(in[i]); ok { b = builder.cs.Add(b, c) } else { - if inTerm := in[i].(expr.Term); !inTerm.Coeff.IsZero() { + if inTerm := in[i].(expr.Term[E]); !inTerm.Coeff.IsZero() { // add only term if coefficient is not zero. - res = append(res, in[i].(expr.Term)) + res = append(res, in[i].(expr.Term[E])) } } } @@ -397,13 +422,13 @@ func (builder *builder) filterConstantSum(in []frontend.Variable) (expr.LinearEx } // returns in split into a slice of compiledTerm and the product of all constants in in as a coeff -func (builder *builder) filterConstantProd(in []frontend.Variable) (expr.LinearExpression, constraint.Element) { - var res expr.LinearExpression +func (builder *builder[E]) filterConstantProd(in []frontend.Variable) (expr.LinearExpression[E], E) { + var res expr.LinearExpression[E] if len(in) <= cap(builder.bufL) { // we can use the temp buffer res = builder.bufL[:0] } else { - res = make(expr.LinearExpression, 0, len(in)) + res = make(expr.LinearExpression[E], 0, len(in)) } b := builder.tOne @@ -411,21 +436,21 @@ func (builder *builder) filterConstantProd(in []frontend.Variable) (expr.LinearE if c, ok := builder.constantValue(in[i]); ok { b = builder.cs.Mul(b, c) } else { - res = append(res, in[i].(expr.Term)) + res = append(res, in[i].(expr.Term[E])) } } return res, b } -func (builder *builder) splitSum(acc expr.Term, r expr.LinearExpression, k *constraint.Element) expr.Term { +func (builder *builder[E]) splitSum(acc expr.Term[E], r expr.LinearExpression[E], k *E) expr.Term[E] { // floor case if len(r) == 0 { if k != nil { // we need to return acc + k - o, found := builder.addConstraintExist(acc, expr.Term{}, *k) + o, found := builder.addConstraintExist(acc, expr.Term[E]{}, *k) if !found { o = builder.newInternalVariable() - builder.addAddGate(acc, expr.Term{}, uint32(o.VID), *k) + builder.addAddGate(acc, expr.Term[E]{}, uint32(o.VID), *k) } return o @@ -434,7 +459,7 @@ func (builder *builder) splitSum(acc expr.Term, r expr.LinearExpression, k *cons } // constraint to add: acc + r[0] (+ k) == o - qC := constraint.Element{} + var qC E if k != nil { qC = *k } @@ -465,7 +490,7 @@ func (builder *builder) splitSum(acc expr.Term, r expr.LinearExpression, k *cons // not going to catch these duplicates. // 2. this piece of code assumes some behavior from constraint/ package (like coeffIDs, or append-style // constraint management) -func (builder *builder) addConstraintExist(a, b expr.Term, k constraint.Element) (expr.Term, bool) { +func (builder *builder[E]) addConstraintExist(a, b expr.Term[E], k E) (expr.Term[E], bool) { // ensure deterministic combined identifier; if a.VID < b.VID { a, b = b, a @@ -480,7 +505,7 @@ func (builder *builder) addConstraintExist(a, b expr.Term, k constraint.Element) // seems likely we have a fit, let's double check inst := builder.cs.GetInstruction(iID) // we know the blueprint we added it. - blueprint := constraint.BlueprintSparseR1CAdd{} + blueprint := constraint.BlueprintSparseR1CAdd[E]{} blueprint.DecompressSparseR1C(&c, inst) // qO == -1 @@ -492,7 +517,7 @@ func (builder *builder) addConstraintExist(a, b expr.Term, k constraint.Element) if tk.CoeffID() != int(c.QC) { // the constant part of the addition differs, no point going forward // since we will need to add a new constraint anyway. - return expr.Term{}, false + return expr.Term[E]{}, false } // check that the coeff matches @@ -503,7 +528,7 @@ func (builder *builder) addConstraintExist(a, b expr.Term, k constraint.Element) if int(c.QL) != ta.CoeffID() || int(c.QR) != tb.CoeffID() { if !k.IsZero() { // may be for some edge cases we could avoid adding a constraint here. - return expr.Term{}, false + return expr.Term[E]{}, false } // we recorded an addition in the form q1*a + q2*b == c // we want to record a new one in the form q3*a + q4*b == n*c @@ -526,7 +551,7 @@ func (builder *builder) addConstraintExist(a, b expr.Term, k constraint.Element) return expr.NewTerm(int(c.XC), q2), true } // we will need an additional constraint - return expr.Term{}, false + return expr.Term[E]{}, false } // we found the same constraint! @@ -535,7 +560,7 @@ func (builder *builder) addConstraintExist(a, b expr.Term, k constraint.Element) // we are going to add this constraint, so we mark it. // ! assumes the caller add an instruction immediately after the call to this function builder.mAddInstructions[h] = builder.cs.GetNbInstructions() - return expr.Term{}, false + return expr.Term[E]{}, false } // mulConstraintExist check if we recorded a constraint in the form @@ -553,7 +578,7 @@ func (builder *builder) addConstraintExist(a, b expr.Term, k constraint.Element) // limitations: // 1. this piece of code assumes some behavior from constraint/ package (like coeffIDs, or append-style // constraint management) -func (builder *builder) mulConstraintExist(a, b expr.Term) (expr.Term, bool) { +func (builder *builder[E]) mulConstraintExist(a, b expr.Term[E]) (expr.Term[E], bool) { // ensure deterministic combined identifier; if a.VID < b.VID { a, b = b, a @@ -568,7 +593,7 @@ func (builder *builder) mulConstraintExist(a, b expr.Term) (expr.Term, bool) { // seems likely we have a fit, let's double check inst := builder.cs.GetInstruction(iID) // we know the blueprint we added it. - blueprint := constraint.BlueprintSparseR1CMul{} + blueprint := constraint.BlueprintSparseR1CMul[E]{} blueprint.DecompressSparseR1C(&c, inst) // qO == -1 @@ -604,10 +629,10 @@ func (builder *builder) mulConstraintExist(a, b expr.Term) (expr.Term, bool) { // we are going to add this constraint, so we mark it. // ! assumes the caller add an instruction immediately after the call to this function builder.mMulInstructions[h] = builder.cs.GetNbInstructions() - return expr.Term{}, false + return expr.Term[E]{}, false } -func (builder *builder) splitProd(acc expr.Term, r expr.LinearExpression) expr.Term { +func (builder *builder[E]) splitProd(acc expr.Term[E], r expr.LinearExpression[E]) expr.Term[E] { // floor case if len(r) == 0 { return acc @@ -629,21 +654,21 @@ func (builder *builder) splitProd(acc expr.Term, r expr.LinearExpression) expr.T // something more like builder.sprintf("my message %le %lv", l0, l1) // to build logs for both debug and println // and append some program location.. (see other todo in debug_info.go) -func (builder *builder) newDebugInfo(errName string, in ...interface{}) constraint.DebugInfo { +func (builder *builder[E]) newDebugInfo(errName string, in ...interface{}) constraint.DebugInfo { for i := 0; i < len(in); i++ { // for inputs that are LinearExpressions or Term, we need to "Make" them in the backend. // TODO @gbotrel this is a duplicate effort with adding a constraint and should be taken care off switch t := in[i].(type) { - case *expr.LinearExpression, expr.LinearExpression: + case *expr.LinearExpression[E], expr.LinearExpression[E]: // shouldn't happen - case expr.Term: + case expr.Term[E]: in[i] = builder.cs.MakeTerm(t.Coeff, t.VID) - case *expr.Term: + case *expr.Term[E]: in[i] = builder.cs.MakeTerm(t.Coeff, t.VID) - case constraint.Element: + case E: in[i] = builder.cs.String(t) - case *constraint.Element: + case *E: in[i] = builder.cs.String(*t) } } @@ -652,29 +677,29 @@ func (builder *builder) newDebugInfo(errName string, in ...interface{}) constrai } -func (builder *builder) Defer(cb func(frontend.API) error) { +func (builder *builder[E]) Defer(cb func(frontend.API) error) { circuitdefer.Put(builder, cb) } // AddInstruction is used to add custom instructions to the constraint system. -func (builder *builder) AddInstruction(bID constraint.BlueprintID, calldata []uint32) []uint32 { +func (builder *builder[E]) AddInstruction(bID constraint.BlueprintID, calldata []uint32) []uint32 { return builder.cs.AddInstruction(bID, calldata) } // AddBlueprint adds a custom blueprint to the constraint system. -func (builder *builder) AddBlueprint(b constraint.Blueprint) constraint.BlueprintID { +func (builder *builder[E]) AddBlueprint(b constraint.Blueprint) constraint.BlueprintID { return builder.cs.AddBlueprint(b) } -func (builder *builder) InternalVariable(wireID uint32) frontend.Variable { +func (builder *builder[E]) InternalVariable(wireID uint32) frontend.Variable { return expr.NewTerm(int(wireID), builder.tOne) } // ToCanonicalVariable converts a frontend.Variable to a constraint system specific Variable // ! Experimental: use in conjunction with constraint.CustomizableSystem -func (builder *builder) ToCanonicalVariable(v frontend.Variable) frontend.CanonicalVariable { +func (builder *builder[E]) ToCanonicalVariable(v frontend.Variable) frontend.CanonicalVariable { switch t := v.(type) { - case expr.Term: + case expr.Term[E]: return builder.cs.MakeTerm(t.Coeff, t.VID) default: c := builder.cs.FromInterface(v) @@ -697,16 +722,18 @@ func (builder *builder) ToCanonicalVariable(v frontend.Variable) frontend.Canoni // // The method only returns a single pair (constraintID, wireLocation) for every // unique wire (removing duplicates). The order of the returned pairs is not the -// same as for the given arguments. -func (builder *builder) GetWireConstraints(wires []frontend.Variable, addMissing bool) ([][2]int, error) { +// same as for the given arguments. It is however, deterministic order. +func (builder *builder[E]) GetWireConstraints(wires []frontend.Variable, addMissing bool) ([][2]int, error) { // construct a lookup table table for later quick access when iterating over instructions lookup := make(map[int]struct{}) - for _, w := range wires { - ww, ok := w.(expr.Term) + wireTerms := make([]expr.Term[E], len(wires)) // stores the term of each wire. + for i, w := range wires { + ww, ok := w.(expr.Term[E]) if !ok { panic("input wire is not a Term") } lookup[ww.WireID()] = struct{}{} + wireTerms[i] = ww } nbPub := builder.cs.GetNbPublicVariables() res := make([][2]int, 0, len(wires)) @@ -731,7 +758,15 @@ func (builder *builder) GetWireConstraints(wires []frontend.Variable, addMissing } if addMissing { nbWitnessWires := builder.cs.GetNbPublicVariables() + builder.cs.GetNbSecretVariables() - for k := range lookup { + // It is important to iterate over wireTerms here as doing it over [lookup] + // would result in a non-deterministic order of constraints. + for _, ww := range wireTerms { + + if _, ok := lookup[ww.WireID()]; !ok { + continue + } + + k := ww.WireID() if k >= nbWitnessWires { return nil, fmt.Errorf("addMissing is true, but wire %d is not a witness", k) } @@ -741,6 +776,7 @@ func (builder *builder) GetWireConstraints(wires []frontend.Variable, addMissing QL: constraint.CoeffIdOne, QO: constraint.CoeffIdMinusOne, }, builder.genericGate) + res = append(res, [2]int{nbPub + constraintIdx, 0}) delete(lookup, k) } @@ -750,3 +786,142 @@ func (builder *builder) GetWireConstraints(wires []frontend.Variable, addMissing } return res, nil } + +// GetWiresConstraintExact works as [GetWireConstraints], but returns an +// exact wire for each constraint. That is if the caller passes the same wire +// several times at different positions in [vars], it will not deduplicate +// unlike [GetWireConstraints]. The function has also a different way to deal +// with constants and missing wires. If the same variabes is passed, then the +// same wire ID is returned. The function returns the first occurrence of the +// wire in the constraint system, by order of the constraints. +// +// - If a variable is a constant. It will introduct an adhoc term and it will +// be reused each time the constant appears. +// +// - The function tolerates that a wire is missing if addMissing is true even +// if the wire is not a witness element. This is allows supporting variables +// that are constrained through hints only. +// +// For instance, +// ``` +// +// GetWiresConstraintsExact([]frontend.Variable{a, a, b, a, c}) => wa, wa, wb, wa, wc +// +// ``` +// +// while, +// +// ``` +// +// GetWiresConstraints([]frontend.Variable{a, a, b, a, c}) => wa, wb, wc +// +// ``` +func (builder *builder[E]) GetWiresConstraintExact(wires []frontend.Variable, addMissing bool) ([][2]int, error) { + + // wireIDsSet stores the indices of all the wires involved in the input. + // We want to ensure that all the stored variables do corresponds to + // canonical variables: therefore not to constants and not too terms + // with a coeff different from 1. This may add constraints but has the + // benefit of making it simpler to read the LRO values. + // + // wireIDsSetOrdered stores the same values as wireIDsSet but in order + // of insertion. This is necessary to ensure the compilation is deterministic + var ( + wireIDsSet = make(map[int]struct{}) + wireTerms = make([]expr.Term[E], len(wires)) + + // constantWiresMap registers the wires that we create to represent + // the constants that appear in the input. It helps avoiding to + // create too many unncessary adhoc terms for the same constant. + constantWiresMap = make(map[E]expr.Term[E]) + ) + + for i, w := range wires { + ww, ok := w.(expr.Term[E]) + if !ok { + // In the case of a Plonk circuit. It will only cover the case + // where "w" was a constant. There, we can assume that this + // condition and the next one are mutually exclusive. + c := builder.cs.FromInterface(w) + o, oWasFound := constantWiresMap[c] + if !oWasFound { + o = builder.newInternalVariable() + constantWiresMap[c] = o + builder.addAddGate(expr.Term[E]{}, expr.Term[E]{}, uint32(o.VID), c) + } + ww = o + } + + if ww.Coeff != builder.tOne { + o := builder.newInternalVariable() + var zero E + builder.addAddGate(ww, expr.Term[E]{}, uint32(o.VID), zero) + ww = o + } + + wireIDsSet[ww.VID] = struct{}{} + wireTerms[i] = ww + } + + // This loop attempts to find the wire IDs in the constraint system and + // gives a localization for each. The loop removes items from [wireIDsSets] + // when they are found. This will allow us to identify the wires that are + // missing from the constraint system. This can happen when wires are + // unconstrained. + var ( + foundWireIDPosition = make(map[int][2]int) + nbPub = builder.cs.GetNbPublicVariables() + iterator = builder.cs.GetSparseR1CIterator() + ) + + for c, constraintIdx := iterator.Next(), 0; c != nil; c, constraintIdx = iterator.Next(), constraintIdx+1 { + if _, ok := wireIDsSet[int(c.XA)]; ok { + foundWireIDPosition[int(c.XA)] = [2]int{nbPub + constraintIdx, 0} + delete(wireIDsSet, int(c.XA)) + } + if _, ok := wireIDsSet[int(c.XB)]; ok { + foundWireIDPosition[int(c.XB)] = [2]int{nbPub + constraintIdx, 1} + delete(wireIDsSet, int(c.XB)) + } + if _, ok := wireIDsSet[int(c.XC)]; ok { + foundWireIDPosition[int(c.XC)] = [2]int{nbPub + constraintIdx, 2} + delete(wireIDsSet, int(c.XC)) + } + if len(wireIDsSet) == 0 { + // we can break early if we found constraints for all the wires + break + } + } + + if addMissing { + for _, ww := range wireTerms { + + // The above loop removes the wireIDs from the set when they are + // found. This means that a wireID is missing if and only if it + // is still in [wireIDsSet]. + if _, isIndeedMissing := wireIDsSet[ww.VID]; !isIndeedMissing { + continue + } + + constraintIdx := builder.cs.AddSparseR1C(constraint.SparseR1C{ + XA: uint32(ww.VID), + XC: uint32(ww.VID), + QL: constraint.CoeffIdOne, + QO: constraint.CoeffIdMinusOne, + }, builder.genericGate) + + foundWireIDPosition[ww.VID] = [2]int{nbPub + constraintIdx, 0} + delete(wireIDsSet, ww.VID) + } + } + + if len(wireIDsSet) > 0 { + return nil, fmt.Errorf("wires not found in constraint system: %v", wireIDsSet) + } + + res := make([][2]int, len(wires)) + for i, w := range wireTerms { + res[i] = foundWireIDPosition[w.VID] + } + return res, nil +} diff --git a/frontend/internal/expr/linear_expression.go b/frontend/internal/expr/linear_expression.go index 8aabe7ed..f0380885 100644 --- a/frontend/internal/expr/linear_expression.go +++ b/frontend/internal/expr/linear_expression.go @@ -5,28 +5,28 @@ import ( "golang.org/x/crypto/blake2b" ) -type LinearExpression []Term +type LinearExpression[E constraint.Element] []Term[E] // NewLinearExpression helper to initialize a linear expression with one term -func NewLinearExpression(vID int, cID constraint.Element) LinearExpression { - return LinearExpression{Term{Coeff: cID, VID: vID}} +func NewLinearExpression[E constraint.Element](vID int, cID E) LinearExpression[E] { + return LinearExpression[E]{Term[E]{Coeff: cID, VID: vID}} } -func (l LinearExpression) Clone() LinearExpression { - res := make(LinearExpression, len(l)) +func (l LinearExpression[E]) Clone() LinearExpression[E] { + res := make(LinearExpression[E], len(l)) copy(res, l) return res } // Len return the length of the Variable (implements Sort interface) -func (l LinearExpression) Len() int { +func (l LinearExpression[E]) Len() int { return len(l) } // Equals returns true if both SORTED expressions are the same // // pre conditions: l and o are sorted -func (l LinearExpression) Equal(o LinearExpression) bool { +func (l LinearExpression[E]) Equal(o LinearExpression[E]) bool { if len(l) != len(o) { return false } @@ -42,19 +42,19 @@ func (l LinearExpression) Equal(o LinearExpression) bool { } // Swap swaps terms in the Variable (implements Sort interface) -func (l LinearExpression) Swap(i, j int) { +func (l LinearExpression[E]) Swap(i, j int) { l[i], l[j] = l[j], l[i] } // Less returns true if variableID for term at i is less than variableID for term at j (implements Sort interface) -func (l LinearExpression) Less(i, j int) bool { +func (l LinearExpression[E]) Less(i, j int) bool { iID := l[i].WireID() jID := l[j].WireID() return iID < jID } // HashCode returns a collision-resistant identifier of the linear expression. It is constructed from the hash codes of the terms. -func (l LinearExpression) HashCode() [16]byte { +func (l LinearExpression[E]) HashCode() [16]byte { h, err := blake2b.New256(nil) if err != nil { panic(err) diff --git a/frontend/internal/expr/term.go b/frontend/internal/expr/term.go index ff803dd5..87744183 100644 --- a/frontend/internal/expr/term.go +++ b/frontend/internal/expr/term.go @@ -7,33 +7,41 @@ import ( "golang.org/x/crypto/blake2b" ) -type Term struct { +type Term[E constraint.Element] struct { VID int - Coeff constraint.Element + Coeff E } -func NewTerm(vID int, cID constraint.Element) Term { - return Term{Coeff: cID, VID: vID} +func NewTerm[E constraint.Element](vID int, cID E) Term[E] { + return Term[E]{Coeff: cID, VID: vID} } -func (t *Term) SetCoeff(c constraint.Element) { +func (t *Term[E]) SetCoeff(c E) { t.Coeff = c } // TODO @gbotrel make that return a uint32 -func (t Term) WireID() int { +func (t Term[E]) WireID() int { return t.VID } // HashCode returns a collision resistant hash code identifier for the term. -func (t Term) HashCode() [16]byte { +func (t Term[E]) HashCode() [16]byte { h, err := blake2b.New256(nil) if err != nil { panic(err) } h.Write(binary.BigEndian.AppendUint64(nil, uint64(t.VID))) - for i := range t.Coeff { - h.Write(binary.BigEndian.AppendUint64(nil, uint64(t.Coeff[i]))) + + switch coeff := any(t.Coeff).(type) { + case constraint.U32: + for i := range coeff { + h.Write(binary.BigEndian.AppendUint32(nil, uint32(coeff[i]))) + } + case constraint.U64: + for i := range coeff { + h.Write(binary.BigEndian.AppendUint64(nil, uint64(coeff[i]))) + } } crc := h.Sum(nil) return [16]byte(crc[:16]) diff --git a/frontend/schema/leaf.go b/frontend/schema/leaf.go index 51d7c493..4c20933f 100644 --- a/frontend/schema/leaf.go +++ b/frontend/schema/leaf.go @@ -1,6 +1,9 @@ package schema -import "reflect" +import ( + "math/big" + "reflect" +) // LeafInfo stores the leaf visibility (always set to Secret or Public) // and the fully qualified name of the path to reach the leaf in the circuit struct. @@ -20,8 +23,20 @@ type LeafCount struct { // LeafHandler is the handler function that will be called when Walk reaches leafs of the struct type LeafHandler func(field LeafInfo, tValue reflect.Value) error -// An object implementing an init hook knows how to "init" itself -// when parsed at compile time -type InitHook interface { - GnarkInitHook() // TODO @gbotrel find a better home for this +// Initializable is an object which knows how to initialize itself when parsed at +// compile time. +// +// This allows to define new primitive circuit variable types which may require +// allocations and by using this interface the circuit user doesn't need to +// explicitly initialize these types themselves. +// +// The Initialize method can be called multiple times during different parsing +// and compilation steps, so the implementation should be idempotent. +type Initializable interface { + // Initialize initializes the object. It receives as an argument the native field + // that will be used to compile the circuit. + // + // NB! This method can be called multiple times, so the implementation should + // be idempotent. + Initialize(field *big.Int) } diff --git a/frontend/schema/schema.go b/frontend/schema/schema.go index 44973617..698224de 100644 --- a/frontend/schema/schema.go +++ b/frontend/schema/schema.go @@ -6,6 +6,7 @@ package schema import ( "fmt" "io" + "math/big" "reflect" "strconv" "strings" @@ -18,22 +19,23 @@ type Schema struct { Fields []Field NbPublic int NbSecret int + Field *big.Int } // New builds a schema.Schema walking through the provided interface (a circuit structure). // // schema.Walk performs better and should be used when possible. -func New(circuit interface{}, tLeaf reflect.Type) (*Schema, error) { +func New(field *big.Int, circuit interface{}, tLeaf reflect.Type) (*Schema, error) { // note circuit is of type interface{} instead of frontend.Circuit to avoid import cycle // same for tLeaf it is in practice always frontend.Variable var nbPublic, nbSecret int - fields, err := parse(nil, circuit, tLeaf, "", "", "", Unset, &nbPublic, &nbSecret) + fields, err := parse(nil, circuit, tLeaf, "", "", "", Unset, &nbPublic, &nbSecret, field) if err != nil { return nil, err } - return &Schema{Fields: fields, NbPublic: nbPublic, NbSecret: nbSecret}, nil + return &Schema{Fields: fields, NbPublic: nbPublic, NbSecret: nbSecret, Field: field}, nil } // Instantiate builds a concrete type using reflect matching the provided schema @@ -80,7 +82,7 @@ func (s Schema) WriteSequence(w io.Writer) error { } return nil } - if _, err := Walk(instance, reflect.TypeOf(a), collectHandler); err != nil { + if _, err := Walk(s.Field, instance, reflect.TypeOf(a), collectHandler); err != nil { return err } @@ -176,7 +178,7 @@ func structTag(baseNameTag string, visibility Visibility, omitEmpty bool) reflec // parentFullName: the name of parent with its ancestors separated by "_" // parentGoName: the name of parent (Go struct definition) // parentTagName: may be empty, set if a struct tag with name is set -func parse(r []Field, input interface{}, target reflect.Type, parentFullName, parentGoName, parentTagName string, parentVisibility Visibility, nbPublic, nbSecret *int) ([]Field, error) { +func parse(r []Field, input interface{}, target reflect.Type, parentFullName, parentGoName, parentTagName string, parentVisibility Visibility, nbPublic, nbSecret *int, field *big.Int) ([]Field, error) { tValue := reflect.ValueOf(input) // get pointed value if needed @@ -275,11 +277,11 @@ func parse(r []Field, input interface{}, target reflect.Type, parentFullName, pa if fValue.CanAddr() && fValue.Addr().CanInterface() { value := fValue.Addr().Interface() - if ih, hasInitHook := value.(InitHook); hasInitHook { - ih.GnarkInitHook() + if ih, hasInitHook := value.(Initializable); hasInitHook { + ih.Initialize(field) } var err error - subFields, err = parse(subFields, value, target, getFullName(parentFullName, name, nameTag), name, nameTag, visibility, nbPublic, nbSecret) + subFields, err = parse(subFields, value, target, getFullName(parentFullName, name, nameTag), name, nameTag, visibility, nbPublic, nbSecret, field) if err != nil { return r, err } @@ -329,7 +331,7 @@ func parse(r []Field, input interface{}, target reflect.Type, parentFullName, pa val := tValue.Index(j) if val.CanAddr() && val.Addr().CanInterface() { fqn := getFullName(parentFullName, strconv.Itoa(j), "") - if _, err := parse(nil, val.Addr().Interface(), target, fqn, fqn, parentTagName, parentVisibility, nbPublic, nbSecret); err != nil { + if _, err := parse(nil, val.Addr().Interface(), target, fqn, fqn, parentTagName, parentVisibility, nbPublic, nbSecret, field); err != nil { return nil, err } } @@ -352,10 +354,10 @@ func parse(r []Field, input interface{}, target reflect.Type, parentFullName, pa if val.CanAddr() && val.Addr().CanInterface() { fqn := getFullName(parentFullName, strconv.Itoa(j), "") ival := val.Addr().Interface() - if ih, hasInitHook := ival.(InitHook); hasInitHook { - ih.GnarkInitHook() + if ih, hasInitHook := ival.(Initializable); hasInitHook { + ih.Initialize(field) } - subFields, err = parse(subFields, ival, target, fqn, fqn, parentTagName, parentVisibility, nbPublic, nbSecret) + subFields, err = parse(subFields, ival, target, fqn, fqn, parentTagName, parentVisibility, nbPublic, nbSecret, field) if err != nil { return nil, err } diff --git a/frontend/schema/schema_test.go b/frontend/schema/schema_test.go index ac7ef667..21ec4426 100644 --- a/frontend/schema/schema_test.go +++ b/frontend/schema/schema_test.go @@ -6,9 +6,12 @@ package schema import ( "bytes" "encoding/json" + "math/big" "reflect" "testing" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/internal/smallfields/tinyfield" "github.com/stretchr/testify/require" ) @@ -90,7 +93,7 @@ func TestSchemaCorrectness(t *testing.T) { // build schema witness := &Circuit{Z: make([]variable, 3)} - s, err := New(witness, tVariable) + s, err := New(ecc.BN254.ScalarField(), witness, tVariable) assert.NoError(err) // instantiate a concrete object @@ -134,7 +137,7 @@ func TestSchemaInherit(t *testing.T) { { var c circuitInherit1 - s, err := Walk(&c, tVariable, nil) + s, err := Walk(ecc.BN254.ScalarField(), &c, tVariable, nil) assert.NoError(err) assert.Equal(2, s.Public) @@ -144,7 +147,7 @@ func TestSchemaInherit(t *testing.T) { { var c circuitInherit2 - s, err := Walk(&c, tVariable, nil) + s, err := Walk(ecc.BN254.ScalarField(), &c, tVariable, nil) assert.NoError(err) assert.Equal(3, s.Public) @@ -156,9 +159,11 @@ type initableVariable struct { Val []variable } -func (iv *initableVariable) GnarkInitHook() { - if iv.Val == nil { +func (iv *initableVariable) Initialize(field *big.Int) { + if field.Cmp(ecc.BN254.ScalarField()) == 0 { iv.Val = make([]variable, 2) + } else { + iv.Val = make([]variable, 3) } } @@ -172,9 +177,13 @@ func TestVariableInitHook(t *testing.T) { assert := require.New(t) witness := &initableCircuit{Y: make([]initableVariable, 2)} - s, err := New(witness, tVariable) + s, err := New(ecc.BN254.ScalarField(), witness, tVariable) assert.NoError(err) assert.Equal(s.NbSecret, 10) // X: 2*2, Y: 2*2, Z: 2 + + s2, err := New(tinyfield.Modulus(), witness, tVariable) + assert.NoError(err) + assert.Equal(s2.NbSecret, 15) // X: 2*3, Y: 2*3, Z: 3 } func BenchmarkLargeSchema(b *testing.B) { @@ -190,7 +199,7 @@ func BenchmarkLargeSchema(b *testing.B) { b.Run("walk", func(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := Walk(&t1, tVariable, nil) + _, err := Walk(ecc.BN254.ScalarField(), &t1, tVariable, nil) if err != nil { b.Fatal(err) } @@ -200,7 +209,7 @@ func BenchmarkLargeSchema(b *testing.B) { b.Run("parse", func(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := New(&t1, tVariable) + _, err := New(ecc.BN254.ScalarField(), &t1, tVariable) if err != nil { b.Fatal(err) } @@ -228,7 +237,7 @@ func BenchmarkArrayOfSliceOfStructSchema(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := Walk(&t1, tVariable, nil) + _, err := Walk(ecc.BN254.ScalarField(), &t1, tVariable, nil) if err != nil { b.Fatal(err) } @@ -238,7 +247,7 @@ func BenchmarkArrayOfSliceOfStructSchema(b *testing.B) { b.Run("parse", func(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := New(&t1, tVariable) + _, err := New(ecc.BN254.ScalarField(), &t1, tVariable) if err != nil { b.Fatal(err) } diff --git a/frontend/schema/tags_test.go b/frontend/schema/tags_test.go index 8c47a1fc..33ae5d70 100644 --- a/frontend/schema/tags_test.go +++ b/frontend/schema/tags_test.go @@ -5,6 +5,7 @@ import ( "reflect" "testing" + "github.com/consensys/gnark-crypto/ecc" "github.com/stretchr/testify/require" ) @@ -22,7 +23,7 @@ func TestStructTags(t *testing.T) { return nil } - _, err := Walk(input, tVariable, collectHandler) + _, err := Walk(ecc.BN254.ScalarField(), input, tVariable, collectHandler) assert.NoError(err) for k, v := range expected { diff --git a/frontend/schema/walk.go b/frontend/schema/walk.go index d71c5981..dc9cbc2f 100644 --- a/frontend/schema/walk.go +++ b/frontend/schema/walk.go @@ -2,6 +2,7 @@ package schema import ( "fmt" + "math/big" "reflect" "strconv" "strings" @@ -13,11 +14,15 @@ import ( // Walk walks through the provided object and stops when it encounters objects of type tLeaf // // It returns the number of secret and public leafs encountered during the walk. -func Walk(circuit interface{}, tLeaf reflect.Type, handler LeafHandler) (count LeafCount, err error) { +// +// The argument field is used to initialize the witness elements (if they +// implement the Initializable interface). +func Walk(field *big.Int, circuit interface{}, tLeaf reflect.Type, handler LeafHandler) (count LeafCount, err error) { w := walker{ target: tLeaf, targetSlice: reflect.SliceOf(tLeaf), handler: handler, + field: field, } err = reflectwalk.Walk(circuit, &w) if err == reflectwalk.ErrSkipEntry { @@ -44,6 +49,7 @@ type walker struct { targetSlice reflect.Type path pathStack nbPublic, nbSecret int + field *big.Int } // Interface handles interface values as they are encountered during the walk. @@ -103,8 +109,8 @@ func (w *walker) arraySliceElem(index int, v reflect.Value) error { // field emulation to "deinitialize" the elements. Maybe we can have a // destructor/deinit hook also? value := v.Addr().Interface() - if ih, hasInitHook := value.(InitHook); hasInitHook { - ih.GnarkInitHook() + if ih, hasInitHook := value.(Initializable); hasInitHook { + ih.Initialize(w.field) } } return nil @@ -175,8 +181,8 @@ func (w *walker) StructField(sf reflect.StructField, v reflect.Value) error { // field emulation to "deinitialize" the elements. Maybe we can have a // destructor/deinit hook also? value := v.Addr().Interface() - if ih, hasInitHook := value.(InitHook); hasInitHook { - ih.GnarkInitHook() + if ih, hasInitHook := value.(Initializable); hasInitHook { + ih.Initialize(w.field) } } diff --git a/frontend/variable.go b/frontend/variable.go index dbaadfab..755854ae 100644 --- a/frontend/variable.go +++ b/frontend/variable.go @@ -4,6 +4,7 @@ package frontend import ( + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend/internal/expr" ) @@ -17,7 +18,9 @@ type Variable interface{} // returned by the API. func IsCanonical(v Variable) bool { switch v.(type) { - case expr.LinearExpression, *expr.LinearExpression, expr.Term, *expr.Term: + case expr.LinearExpression[constraint.U32], *expr.LinearExpression[constraint.U32], expr.Term[constraint.U32], *expr.Term[constraint.U32]: + return true + case expr.LinearExpression[constraint.U64], *expr.LinearExpression[constraint.U64], expr.Term[constraint.U64], *expr.Term[constraint.U64]: return true } return false diff --git a/frontend/witness.go b/frontend/witness.go index b0eecd42..2bf63f7e 100644 --- a/frontend/witness.go +++ b/frontend/witness.go @@ -20,7 +20,7 @@ func NewWitness(assignment Circuit, field *big.Int, opts ...WitnessOption) (witn } // count the leaves - s, err := schema.Walk(assignment, tVariable, nil) + s, err := schema.Walk(field, assignment, tVariable, nil) if err != nil { return nil, err } @@ -38,14 +38,14 @@ func NewWitness(assignment Circuit, field *big.Int, opts ...WitnessOption) (witn chValues := make(chan any) go func() { defer close(chValues) - schema.Walk(assignment, tVariable, func(leaf schema.LeafInfo, tValue reflect.Value) error { + schema.Walk(field, assignment, tVariable, func(leaf schema.LeafInfo, tValue reflect.Value) error { if leaf.Visibility == schema.Public { chValues <- tValue.Interface() } return nil }) if !opt.publicOnly { - schema.Walk(assignment, tVariable, func(leaf schema.LeafInfo, tValue reflect.Value) error { + schema.Walk(field, assignment, tVariable, func(leaf schema.LeafInfo, tValue reflect.Value) error { if leaf.Visibility == schema.Secret { chValues <- tValue.Interface() } @@ -63,8 +63,8 @@ func NewWitness(assignment Circuit, field *big.Int, opts ...WitnessOption) (witn // NewSchema returns the schema corresponding to the circuit structure. // // This is used to JSON (un)marshall witnesses. -func NewSchema(circuit Circuit) (*schema.Schema, error) { - return schema.New(circuit, tVariable) +func NewSchema(field *big.Int, circuit Circuit) (*schema.Schema, error) { + return schema.New(field, circuit, tVariable) } // default options diff --git a/go.mod b/go.mod index 8296d75b..476521b4 100644 --- a/go.mod +++ b/go.mod @@ -1,37 +1,39 @@ module github.com/consensys/gnark -go 1.22 +go 1.23.0 -toolchain go1.22.6 +toolchain go1.23.8 require ( - github.com/bits-and-blooms/bitset v1.20.0 + github.com/bits-and-blooms/bitset v1.22.0 github.com/blang/semver/v4 v4.0.0 - github.com/consensys/bavard v0.1.29 + github.com/consensys/bavard v0.1.31-0.20250406004941-2db259e4b582 github.com/consensys/compress v0.2.5 - github.com/consensys/gnark-crypto v0.16.1-0.20250217214835-5ed804970f85 - github.com/fxamacker/cbor/v2 v2.7.0 - github.com/google/go-cmp v0.6.0 - github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 + github.com/consensys/gnark-crypto v0.18.0 + github.com/fxamacker/cbor/v2 v2.8.0 + github.com/google/go-cmp v0.7.0 + github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a github.com/icza/bitio v1.1.0 - github.com/ingonyama-zk/icicle/v3 v3.1.1-0.20241118092657-fccdb2f0921b + github.com/ingonyama-zk/icicle-gnark/v3 v3.2.2 github.com/leanovate/gopter v0.2.11 - github.com/ronanh/intcomp v1.1.0 - github.com/rs/zerolog v1.33.0 + github.com/ronanh/intcomp v1.1.1 + github.com/rs/zerolog v1.34.0 github.com/stretchr/testify v1.10.0 - golang.org/x/crypto v0.33.0 - golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 - golang.org/x/sync v0.11.0 + golang.org/x/crypto v0.39.0 + golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 + golang.org/x/sync v0.15.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/sys v0.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) + +replace github.com/consensys/gnark-crypto => github.com/polyhedrazk/gnark-crypto v0.18.1-0.20250720223224-8b1cee1ff224 diff --git a/go.sum b/go.sum index c31ddf14..5f62614a 100644 --- a/go.sum +++ b/go.sum @@ -44,8 +44,8 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= -github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= -github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4= +github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= @@ -57,12 +57,10 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/consensys/bavard v0.1.29 h1:fobxIYksIQ+ZSrTJUuQgu+HIJwclrAPcdXqd7H2hh1k= -github.com/consensys/bavard v0.1.29/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= +github.com/consensys/bavard v0.1.31-0.20250406004941-2db259e4b582 h1:dTlIwEdFQmldzFf5F6bbTcYWhvnAgZai2g8eq3Wwxqg= +github.com/consensys/bavard v0.1.31-0.20250406004941-2db259e4b582/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= github.com/consensys/compress v0.2.5 h1:gJr1hKzbOD36JFsF1AN8lfXz1yevnJi1YolffY19Ntk= github.com/consensys/compress v0.2.5/go.mod h1:pyM+ZXiNUh7/0+AUjUf9RKUM6vSH7T/fsn5LLS0j1Tk= -github.com/consensys/gnark-crypto v0.16.1-0.20250217214835-5ed804970f85 h1:3ht4gGH3smFGVLFhpFTKvDbEdagC6eSaPXnHjCQGh94= -github.com/consensys/gnark-crypto v0.16.1-0.20250217214835-5ed804970f85/go.mod h1:A2URlMHUT81ifJ0UlLzSlm7TmnE3t7VxEThApdMukJw= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -79,8 +77,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= +github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -130,8 +128,8 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -147,8 +145,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= -github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18= +github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -184,8 +182,8 @@ github.com/icza/bitio v1.1.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6 h1:8UsGZ2rr2ksmEru6lToqnXgA8Mz1DP11X4zSJ159C3k= github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/ingonyama-zk/icicle/v3 v3.1.1-0.20241118092657-fccdb2f0921b h1:AvQTK7l0PTHODD06PVQX1Tn2o29sRIaKIDOvTJmKurY= -github.com/ingonyama-zk/icicle/v3 v3.1.1-0.20241118092657-fccdb2f0921b/go.mod h1:e0JHb27/P6WorCJS3YolbY5XffS4PGBuoW38OthLkDs= +github.com/ingonyama-zk/icicle-gnark/v3 v3.2.2 h1:B+aWVgAx+GlFLhtYjIaF0uGjU3rzpl99Wf9wZWt+Mq8= +github.com/ingonyama-zk/icicle-gnark/v3 v3.2.2/go.mod h1:CH/cwcr21pPWH+9GtK/PFaa4OGTv4CtfkCKro6GpbRE= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -204,8 +202,9 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -235,17 +234,19 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/polyhedrazk/gnark-crypto v0.18.1-0.20250720223224-8b1cee1ff224 h1:W5IEieLCnmUGRy5jXSWYyEy/tvGN6Ndd6CPR2cmo4ms= +github.com/polyhedrazk/gnark-crypto v0.18.1-0.20250720223224-8b1cee1ff224/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/ronanh/intcomp v1.1.0 h1:i54kxmpmSoOZFcWPMWryuakN0vLxLswASsGa07zkvLU= -github.com/ronanh/intcomp v1.1.0/go.mod h1:7FOLy3P3Zj3er/kVrU/pl+Ql7JFZj7bwliMGketo0IU= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= -github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/ronanh/intcomp v1.1.1 h1:+1bGV/wEBiHI0FvzS7RHgzqOpfbBJzLIxkqMJ9e6yxY= +github.com/ronanh/intcomp v1.1.1/go.mod h1:7FOLy3P3Zj3er/kVrU/pl+Ql7JFZj7bwliMGketo0IU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -304,8 +305,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -316,8 +317,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= -golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 h1:bsqhLWFR6G6xiQcb+JoGqdKdRU6WzPWmK8E0jxTjzo4= +golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -410,8 +411,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= -golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -462,8 +463,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/internal/backend/circuits/commit.go b/internal/backend/circuits/commit.go index 792f534a..f0bd2dc9 100644 --- a/internal/backend/circuits/commit.go +++ b/internal/backend/circuits/commit.go @@ -4,6 +4,7 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bn254" "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/smallfields" ) type commitCircuit struct { @@ -13,11 +14,21 @@ type commitCircuit struct { func (circuit *commitCircuit) Define(api frontend.API) error { api.AssertIsDifferent(circuit.Public, 0) - commitment, err := api.(frontend.Committer).Commit(circuit.X, circuit.Public, 5) - if err != nil { - return err + if !smallfields.IsSmallField(api.Compiler().Field()) { + commitment, err := api.(frontend.Committer).Commit(circuit.X, circuit.Public, 5) + if err != nil { + return err + } + api.AssertIsDifferent(commitment, 0) + } else { + commitment, err := api.(frontend.WideCommitter).WideCommit(2, circuit.X, circuit.Public, 5) + if err != nil { + return err + } + for i := range commitment { + api.AssertIsDifferent(commitment[i], 0) + } } - api.AssertIsDifferent(commitment, 0) a := api.Mul(circuit.X, circuit.X) for i := 0; i < 10; i++ { a = api.Mul(a, circuit.X) diff --git a/internal/generator/backend/main.go b/internal/generator/backend/main.go index 027ed4c4..d07f64a8 100644 --- a/internal/generator/backend/main.go +++ b/internal/generator/backend/main.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "os" "os/exec" "path/filepath" @@ -20,66 +21,84 @@ var bgen = bavard.NewBatchGenerator(copyrightHolder, 2020, "gnark") func main() { bls12_377 := templateData{ - RootPath: "../../../backend/{?}/bls12-377/", - CSPath: "../../../constraint/bls12-377/", - Curve: "BLS12-377", - CurveID: "BLS12_377", + RootPath: "../../../backend/{?}/bls12-377/", + CSPath: "../../../constraint/bls12-377/", + Curve: "BLS12-377", + CurveID: "BLS12_377", + ElementType: "U64", } bls12_381 := templateData{ - RootPath: "../../../backend/{?}/bls12-381/", - CSPath: "../../../constraint/bls12-381/", - Curve: "BLS12-381", - CurveID: "BLS12_381", + RootPath: "../../../backend/{?}/bls12-381/", + CSPath: "../../../constraint/bls12-381/", + Curve: "BLS12-381", + CurveID: "BLS12_381", + ElementType: "U64", } bn254 := templateData{ - RootPath: "../../../backend/{?}/bn254/", - CSPath: "../../../constraint/bn254/", - Curve: "BN254", - CurveID: "BN254", + RootPath: "../../../backend/{?}/bn254/", + CSPath: "../../../constraint/bn254/", + Curve: "BN254", + CurveID: "BN254", + ElementType: "U64", } bw6_761 := templateData{ - RootPath: "../../../backend/{?}/bw6-761/", - CSPath: "../../../constraint/bw6-761/", - Curve: "BW6-761", - CurveID: "BW6_761", + RootPath: "../../../backend/{?}/bw6-761/", + CSPath: "../../../constraint/bw6-761/", + Curve: "BW6-761", + CurveID: "BW6_761", + ElementType: "U64", } bls24_315 := templateData{ - RootPath: "../../../backend/{?}/bls24-315/", - CSPath: "../../../constraint/bls24-315/", - Curve: "BLS24-315", - CurveID: "BLS24_315", + RootPath: "../../../backend/{?}/bls24-315/", + CSPath: "../../../constraint/bls24-315/", + Curve: "BLS24-315", + CurveID: "BLS24_315", + ElementType: "U64", } bls24_317 := templateData{ - RootPath: "../../../backend/{?}/bls24-317/", - CSPath: "../../../constraint/bls24-317/", - Curve: "BLS24-317", - CurveID: "BLS24_317", + RootPath: "../../../backend/{?}/bls24-317/", + CSPath: "../../../constraint/bls24-317/", + Curve: "BLS24-317", + CurveID: "BLS24_317", + ElementType: "U64", } bw6_633 := templateData{ - RootPath: "../../../backend/{?}/bw6-633/", - CSPath: "../../../constraint/bw6-633/", - Curve: "BW6-633", - CurveID: "BW6_633", + RootPath: "../../../backend/{?}/bw6-633/", + CSPath: "../../../constraint/bw6-633/", + Curve: "BW6-633", + CurveID: "BW6_633", + ElementType: "U64", } tiny_field := templateData{ - RootPath: "../../../internal/tinyfield/", - CSPath: "../../../constraint/tinyfield", - Curve: "tinyfield", - CurveID: "UNKNOWN", - noBackend: true, - NoGKR: true, + RootPath: "../../../internal/smallfields/tinyfield/", + CSPath: "../../../constraint/tinyfield", + Curve: "tinyfield", + CurveID: "UNKNOWN", + noBackend: true, + NoGKR: true, + AutoGenerateField: "0x2f", + ElementType: "U32", } - - // autogenerate tinyfield - tinyfieldConf, err := config.NewFieldConfig("tinyfield", "Element", "0x2f", false) - if err != nil { - panic(err) + baby_bear_field := templateData{ + CSPath: "../../../constraint/babybear/", + Curve: "babybear", + CurveID: "UNKNOWN", + OnlyField: true, + noBackend: true, + NoGKR: true, + ElementType: "U32", } - if err := generator.GenerateFF(tinyfieldConf, tiny_field.RootPath); err != nil { - panic(err) + koala_bear_field := templateData{ + CSPath: "../../../constraint/koalabear/", + Curve: "koalabear", + CurveID: "UNKNOWN", + OnlyField: true, + noBackend: true, + NoGKR: true, + ElementType: "U32", } - datas := []templateData{ + data := []templateData{ bls12_377, bls12_381, bn254, @@ -88,18 +107,29 @@ func main() { bls24_317, bw6_633, tiny_field, + baby_bear_field, + koala_bear_field, } const importCurve = "../imports.go.tmpl" - var wg sync.WaitGroup - for _, d := range datas { + for _, d := range data { wg.Add(1) go func(d templateData) { defer wg.Done() + // auto-generate small fields + if d.AutoGenerateField != "" { + conf, err := config.NewFieldConfig(d.Curve, "Element", d.AutoGenerateField, false) + if err != nil { + panic(err) + } + if err := generator.GenerateFF(conf, d.RootPath, generator.WithASM(nil)); err != nil { + panic(err) + } + } var ( groth16Dir = strings.Replace(d.RootPath, "{?}", "groth16", 1) @@ -107,13 +137,6 @@ func main() { plonkDir = strings.Replace(d.RootPath, "{?}", "plonk", 1) ) - if err := os.MkdirAll(groth16Dir, 0700); err != nil { - panic(err) - } - if err := os.MkdirAll(plonkDir, 0700); err != nil { - panic(err) - } - csDir := d.CSPath // constraint systems @@ -128,11 +151,21 @@ func main() { } // gkr backend - if d.Curve != "tinyfield" { - entries = []bavard.Entry{{File: filepath.Join(csDir, "gkr.go"), Templates: []string{"gkr.go.tmpl", importCurve}}} - if err := bgen.Generate(d, "cs", "./template/representations/", entries...); err != nil { - panic(err) + if !d.NoGKR { + curvePackageName := strings.ToLower(d.Curve) + + cfg := gkrConfig{ + FieldDependency: config.FieldDependency{ + ElementType: "fr.Element", + FieldPackageName: "fr", + FieldPackagePath: "github.com/consensys/gnark-crypto/ecc/" + curvePackageName + "/fr", + }, + FieldID: d.CurveID, + GkrPackageName: curvePackageName, + CanUseFFT: true, } + + assertNoError(generateGkrBackend(cfg)) } entries = []bavard.Entry{ @@ -203,23 +236,110 @@ func main() { } + wg.Add(1) + // GKR test vectors + go func() { + // generate gkr and sumcheck for small-rational + cfg := gkrConfig{ + FieldDependency: config.FieldDependency{ + ElementType: "small_rational.SmallRational", + FieldPackagePath: "github.com/consensys/gnark/internal/small_rational", + FieldPackageName: "small_rational", + }, + GkrPackageName: "small_rational", + CanUseFFT: false, + NoGkrTests: true, + GenerateTestVectors: true, + } + assertNoError(generateGkrBackend(cfg)) + + fmt.Println("generating test vectors for gkr and sumcheck") + runCmd("go", "run", "../../gkr/test_vectors") + wg.Done() + }() + wg.Wait() - // run go fmt on whole directory - cmd := exec.Command("gofmt", "-s", "-w", "../../../") + // run gofmt on whole directory + runCmd("gofmt", "-w", "../../../") + + // run goimports on whole directory + runCmd("goimports", "-w", "../../../") +} + +func runCmd(name string, arg ...string) { + fmt.Println(name, strings.Join(arg, " ")) + cmd := exec.Command(name, arg...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - if err := cmd.Run(); err != nil { - panic(err) + assertNoError(cmd.Run()) +} + +type templateData struct { + RootPath string + CSPath string + Curve string + CurveID string + + AutoGenerateField string // the field implementation will be generated. Field value should be field modulus in hex (starting with 0x prefix) + OnlyField bool // use field from gnark-crypto. Import package is deduced from Curve field + noBackend bool + NoGKR bool + ElementType string +} + +func generateGkrBackend(cfg gkrConfig) error { + packageDir := filepath.Join("../../../internal/gkr", cfg.GkrPackageName) + + testVectorUtilsFileName := "test_vector_utils_test.go" + if cfg.GenerateTestVectors { + testVectorUtilsFileName = "test_vector_utils.go" // needs to be accessible to two separate packages + } + + // gkr backend + entries := []bavard.Entry{ + {File: filepath.Join(packageDir, "gkr.go"), Templates: []string{"gkr.go.tmpl"}}, + {File: filepath.Join(packageDir, "gate_testing.go"), Templates: []string{"gate_testing.go.tmpl"}}, + {File: filepath.Join(packageDir, "sumcheck.go"), Templates: []string{"sumcheck.go.tmpl"}}, + {File: filepath.Join(packageDir, "sumcheck_test.go"), Templates: []string{"sumcheck.test.go.tmpl", "sumcheck.test.defs.go.tmpl"}}, + {File: filepath.Join(packageDir, testVectorUtilsFileName), Templates: []string{"test_vector_utils.go.tmpl"}}, + } + + if !cfg.NoGkrTests { + entries = append(entries, bavard.Entry{ + File: filepath.Join(packageDir, "gkr_test.go"), Templates: []string{"gkr.test.go.tmpl", "gkr.test.vectors.go.tmpl"}, + }) + } + + if cfg.GenerateTestVectors { + entries = append(entries, []bavard.Entry{ + {File: filepath.Join(packageDir, "test_vector_gen.go"), Templates: []string{"gkr.test.vectors.gen.go.tmpl", "gkr.test.vectors.go.tmpl"}}, + {File: filepath.Join(packageDir, "sumcheck_test_vector_gen.go"), Templates: []string{"sumcheck.test.vectors.gen.go.tmpl", "sumcheck.test.defs.go.tmpl"}}, + }...) + } else { + entries = append(entries, bavard.Entry{ + File: filepath.Join(packageDir, "solver_hints.go"), Templates: []string{"solver_hints.go.tmpl"}, + }) } + if err := bgen.Generate(cfg, "gkr", "./template/gkr/", entries...); err != nil { + return err + } + + return nil } -type templateData struct { - RootPath string - CSPath string - Curve string - CurveID string - noBackend bool - NoGKR bool +type gkrConfig struct { + config.FieldDependency + GkrPackageName string // the GKR package, relative to the repo root + FieldID string // e.g. BLS12_377, BABYBEAR, etc. + CanUseFFT bool + GenerateTestVectors bool + NoGkrTests bool +} + +func assertNoError(err error) { + if err != nil { + panic(err) + } } diff --git a/internal/generator/backend/template/gkr/gate_testing.go.tmpl b/internal/generator/backend/template/gkr/gate_testing.go.tmpl new file mode 100644 index 00000000..534b4b01 --- /dev/null +++ b/internal/generator/backend/template/gkr/gate_testing.go.tmpl @@ -0,0 +1,213 @@ +import ( + "fmt" + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + + "github.com/consensys/gnark/std/gkrapi/gkr" + "{{.FieldPackagePath}}" + {{- if .CanUseFFT }} + "{{.FieldPackagePath}}/fft" + "sync"{{- else}} + "errors"{{- end }} + "{{.FieldPackagePath}}/polynomial" + "slices" +) + +// IsGateFunctionAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func IsGateFunctionAdditive(f gkr.GateFunction, i, nbIn int) bool { + fWrapped := api.convertFunc(f) + + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make({{.FieldPackageName}}.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 := fWrapped(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := fWrapped(in...) + + y2.Sub(y2, y1) + y1.Sub(y1, y0) + + if !y2.Equal(y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 = fWrapped(in...) + + y1.Sub(y1, y0) + + return y1.Equal(y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f gateFunctionFr) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]{{ .ElementType }}, nbIn) + consts := make({{.FieldPackageName}}.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + {{- if .CanUseFFT }} + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := {{.FieldPackageName}}.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i].Set(f(fIn...)) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + {{- else }} + x := make({{.FieldPackageName}}.Vector, degreeBound) + x.MustSetRandom() + for i := range x { + fIn[0] = x[i] + for j := range consts { + fIn[j+1].Mul(&x[i], &consts[j]) + } + p[i].Set(f(fIn...)) + } + + // obtain p's coefficients + p, err := interpolate(x, p) + if err != nil { + panic(err) + } + {{- end }} + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +// FindGateFunctionDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func FindGateFunctionDegree(f gkr.GateFunction, max, nbIn int) (int, error) { + fFr := api.convertFunc(f) + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := fFr.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, gkrtypes.ErrZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func VerifyGateFunctionDegree(f gkr.GateFunction, claimedDegree, nbIn int) error { + fFr := api.convertFunc(f) + if p := fFr.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return gkrtypes.ErrZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} + +{{- if not .CanUseFFT }} +// interpolate fits a polynomial of degree len(X) - 1 = len(Y) - 1 to the points (X[i], Y[i]) +// Note that the runtime is O(len(X)³) +func interpolate(X, Y []{{.ElementType}}) (polynomial.Polynomial, error) { + if len(X) != len(Y) { + return nil, errors.New("X and Y must have the same length") + } + + // solve the system of equations by Gaussian elimination + augmentedRows := make([][]{{.ElementType}}, len(X)) // the last column is the Y values + for i := range augmentedRows { + augmentedRows[i] = make([]{{.ElementType}}, len(X)+1) + augmentedRows[i][0].SetOne() + augmentedRows[i][1].Set(&X[i]) + for j := 2; j < len(augmentedRows[i])-1; j++ { + augmentedRows[i][j].Mul(&augmentedRows[i][j-1], &X[i]) + } + augmentedRows[i][len(augmentedRows[i])-1].Set(&Y[i]) + } + + // make the upper triangle + for i := range len(augmentedRows) - 1 { + // use row i to eliminate the ith element in all rows below + var negInv {{.ElementType}} + if augmentedRows[i][i].IsZero() { + return nil, errors.New("singular matrix") + } + negInv.Inverse(&augmentedRows[i][i]) + negInv.Neg(&negInv) + for j := i + 1; j < len(augmentedRows); j++ { + var c {{.ElementType}} + c.Mul(&augmentedRows[j][i], &negInv) + // augmentedRows[j][i].SetZero() omitted + for k := i + 1; k < len(augmentedRows[i]); k++ { + var t {{.ElementType}} + t.Mul(&augmentedRows[i][k], &c) + augmentedRows[j][k].Add(&augmentedRows[j][k], &t) + } + } + } + + // back substitution + res := make(polynomial.Polynomial, len(X)) + for i := len(augmentedRows) - 1; i >= 0; i-- { + res[i] = augmentedRows[i][len(augmentedRows[i])-1] + for j := i + 1; j < len(augmentedRows[i])-1; j++ { + var t {{.ElementType}} + t.Mul(&res[j], &augmentedRows[i][j]) + res[i].Sub(&res[i], &t) + } + res[i].Div(&res[i], &augmentedRows[i][i]) + } + + return res, nil +} +{{- end }} \ No newline at end of file diff --git a/internal/generator/backend/template/gkr/gkr.go.tmpl b/internal/generator/backend/template/gkr/gkr.go.tmpl new file mode 100644 index 00000000..5105b0a3 --- /dev/null +++ b/internal/generator/backend/template/gkr/gkr.go.tmpl @@ -0,0 +1,812 @@ +import ( + "errors" + "fmt" + "{{.FieldPackagePath}}" + "{{.FieldPackagePath}}/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark-crypto/utils" + "math/big" + "strconv" + "sync" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// The goal is to prove/verify evaluations of many instances of the same circuit + + +// WireAssignment is assignment of values to the same wire across many instances of the circuit +type WireAssignment []polynomial.MultiLin + +type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) + +// eqTimesGateEvalSumcheckLazyClaims is a lazy claim for sumcheck (verifier side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the checking of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckLazyClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]{{ .ElementType }} // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []{{ .ElementType }} // yᵢ = w(xᵢ), allegedly + manager *claimsManager // WARNING: Circular references +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) getWire() *gkrtypes.Wire { + return e.manager.wires[e.wireI] +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) claimsNum() int { + return len(e.evaluationPoints) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) varsNum() int { + return len(e.evaluationPoints[0]) +} + +// combinedSum returns ∑ᵢ aⁱ yᵢ +func (e *eqTimesGateEvalSumcheckLazyClaims) combinedSum(a {{ .ElementType }}) {{ .ElementType }} { + evalsAsPoly := polynomial.Polynomial(e.claimedEvaluations) + return evalsAsPoly.Eval(&a) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) degree(int) int { + return 1 + e.manager.wires[e.wireI].Gate.Degree() +} + +// verifyFinalEval finalizes the verification of w. +// The prover's claims w(xᵢ) = yᵢ have already been reduced to verifying +// ∑ cⁱ eq(xᵢ, r) w(r) = purportedValue. ( c is combinationCoeff ) +// Both purportedValue and the vector r have been randomized during the sumcheck protocol. +// By taking the w term out of the sum we get the equivalent claim that +// for E := ∑ eq(xᵢ, r), it must be that E w(r) = purportedValue. +// If w is an input wire, the verifier can directly check its evaluation at r. +// Otherwise, the prover makes claims about the evaluation of w's input wires, +// wᵢ, at r, to be verified later. +// The claims are communicated through the proof parameter. +// The verifier checks here if the claimed evaluations of wᵢ(r) are consistent with +// the main claim, by checking E w(wᵢ(r)...) = purportedValue. +func (e *eqTimesGateEvalSumcheckLazyClaims) verifyFinalEval(r []{{ .ElementType }}, combinationCoeff, purportedValue {{ .ElementType }}, uniqueInputEvaluations []{{ .ElementType }}) error { + // the eq terms ( E ) + numClaims := len(e.evaluationPoints) + evaluation := polynomial.EvalEq(e.evaluationPoints[numClaims-1], r) + for i := numClaims - 2; i >= 0; i-- { + evaluation.Mul(&evaluation, &combinationCoeff) + eq := polynomial.EvalEq(e.evaluationPoints[i], r) + evaluation.Add(&evaluation, &eq) + } + + wire := e.manager.wires[e.wireI] + + // the w(...) term + var gateEvaluation {{ .ElementType }} + if wire.IsInput() { // just compute w(r) + gateEvaluation = e.manager.assignment[e.wireI].Evaluate(r, e.manager.memPool) + } else { // proof contains the evaluations of the inputs, but avoids repetition in case multiple inputs come from the same wire + injection, injectionLeftInv := + e.manager.wires.ClaimPropagationInfo(e.wireI) + + if len(injection) != len(uniqueInputEvaluations) { + return fmt.Errorf("%d input wire evaluations given, %d expected", len(uniqueInputEvaluations), len(injection)) + } + + for uniqueI, i := range injection { // map from unique to all + e.manager.add(wire.Inputs[i], r, uniqueInputEvaluations[uniqueI]) + } + + inputEvaluations := make([]frontend.Variable, len(wire.Inputs)) + for i, uniqueI := range injectionLeftInv { // map from all to unique + inputEvaluations[i] = &uniqueInputEvaluations[uniqueI] + } + + gateEvaluation.Set(wire.Gate.Evaluate(api, inputEvaluations...).(*{{ .ElementType }})) + } + + evaluation.Mul(&evaluation, &gateEvaluation) + + if evaluation.Equal(&purportedValue) { + return nil + } + return errors.New("incompatible evaluations") +} + +// eqTimesGateEvalSumcheckClaims is a claim for sumcheck (prover side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the proving of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]{{ .ElementType }} // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []{{ .ElementType }} // yᵢ = w(xᵢ) + manager *claimsManager + + input []polynomial.MultiLin // input[i](h₁, ..., hₘ₋ⱼ) = wᵢ(r₁, r₂, ..., rⱼ₋₁, h₁, ..., hₘ₋ⱼ) + + eq polynomial.MultiLin // E := ∑ᵢ cⁱ eq(xᵢ, -) +} + +func (c *eqTimesGateEvalSumcheckClaims) getWire() *gkrtypes.Wire { + return c.manager.wires[c.wireI] +} + +// combine the multiple claims into one claim using a random combination (combinationCoeff or c). +// From the original multiple claims of w(xᵢ) = yᵢ, we get a single claim +// ∑ᵢ,ₕ cⁱ eq(xᵢ, h) w(h) = ∑ᵢ cⁱ yᵢ, where h iterates over the hypercube (circuit instances) and +// i iterates over the claims. +// Equivalently, we could say ∑ᵢ cⁱ yᵢ = ∑ₕ,ᵢ cⁱ eq(xᵢ, h) w(h) = ∑ₕ w(h) ∑ᵢ cⁱ eq(xᵢ, h). +// Thus if we initially compute E := ∑ᵢ cⁱ eq(xᵢ, -), our claim will find the simpler form +// ∑ᵢ cⁱ yᵢ = ∑ₕ w(h) E(h), where the sum-checked polynomial is of degree deg(g) + 1, +// and deg(g) is the total degree of the polynomial defining the gate g of which w is the output. +// The output of combine is the first sumcheck claim, i.e. ∑₍ₕ₁,ₕ₂,...₎ w(X, h₁, h₂, ...) E(X, h₁, h₂, ...).. +func (c *eqTimesGateEvalSumcheckClaims) combine(combinationCoeff {{ .ElementType }}) polynomial.Polynomial { +varsNum := c.varsNum() + eqLength := 1 << varsNum +claimsNum := c.claimsNum() +// initialize the eq tables ( E ) +c.eq = c.manager.memPool.Make(eqLength) + +c.eq[0].SetOne() +c.eq.Eq(c.evaluationPoints[0]) + +// E := eq(x₀, -) +newEq := polynomial.MultiLin(c.manager.memPool.Make(eqLength)) +aI := combinationCoeff + +// E += cⁱ eq(xᵢ, -) +for k := 1; k < claimsNum; k++ { +newEq[0].Set(&aI) + +c.eqAcc(c.eq, newEq,c.evaluationPoints[k]) + +if k+1 < claimsNum { +aI.Mul(&aI, &combinationCoeff) +} +} + +c.manager.memPool.Dump(newEq) + +return c.computeGJ() +} + +// eqAcc sets m to an eq table at q and then adds it to e. +// m <- eq(q, -). +// e <- e + m +func (c *eqTimesGateEvalSumcheckClaims) eqAcc(e, m polynomial.MultiLin, q []{{ .ElementType }}) { + n := len(q) + + //At the end of each iteration, m(h₁, ..., hₙ) = eq(q₁, ..., qᵢ₊₁, h₁, ..., hᵢ₊₁) + for i := range q { // In the comments we use a 1-based index so q[i] = qᵢ₊₁ + // go through all assignments of (b₁, ..., bᵢ) ∈ {0,1}ⁱ + const threshold = 1 << 6 + k := 1 << i + if k < threshold { + for j := 0; j < k; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + } else { + c.manager.workers.Submit(k, func(start, end int) { + for j := start; j < end; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + }, 1024).Wait() + } + + } + c.manager.workers.Submit(len(e), func(start, end int) { + for i := start; i < end; i++ { + e[i].Add(&e[i], &m[i]) + } + }, 512).Wait() +} + + +// computeGJ: gⱼ = ∑_{0≤h<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, h...) = ∑_{0≤i<2ⁿ⁻ʲ} E(r₁, ..., Xⱼ, h...) g( w₀(r₁, ..., Xⱼ, h...), ... ). +// the polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). +// The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). By convention, g₀ is a constant polynomial equal to the claimed sum. +func (c *eqTimesGateEvalSumcheckClaims) computeGJ() polynomial.Polynomial { + + wire := c.getWire() + degGJ := 1 + wire.Gate.Degree() // guaranteed to be no smaller than the actual deg(gⱼ) + nbGateIn := len(c.input) + + // Both E and wᵢ (the input wires and the eq table) are multilinear, thus + // they are linear in Xⱼ. + // So for f ∈ { E(r₁, ..., Xⱼ, h...) } ∪ {wᵢ(r₁, ..., Xⱼ, h...) }, so f(m) = m×(f(1) - f(0)) + f(0), and f(0), f(1) are easily computed from the bookkeeping tables. + // ml are such multilinear polynomials the evaluations of which over different values of Xⱼ are computed in this stepwise manner. + ml := make([]polynomial.MultiLin, nbGateIn+1) // shortcut to the evaluations of the multilinear polynomials over the hypercube + ml[0] = c.eq + copy(ml[1:], c.input) + + sumSize := len(c.eq) / 2; // the range of h, over which we sum + + // Perf-TODO: Collate once at claim "combination" time and not again. then, even folding can be done in one operation every time "next" is called + + gJ := make([]{{ .ElementType }}, degGJ) + var mu sync.Mutex + computeAll := func(start, end int) { // compute method to allow parallelization across instances + var step {{ .ElementType }} + + res := make([]{{ .ElementType }}, degGJ) + + // evaluations of ml, laid out as: + // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), + // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), + // ... + // ml[0](degGJ, h...), ml[2](degGJ, h...), ..., ml[len(ml)-1](degGJ, h...) + mlEvals := make([]{{ .ElementType }}, degGJ*len(ml)) + gateInput := make([]frontend.Variable, nbGateIn) + + for h := start; h < end; h++ { // h counts across instances + + evalAt1Index := sumSize + h + for k := range ml { + // d = 0 + mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1. Can be taken directly from the table. + step.Sub(&mlEvals[k], &ml[k][h])// step = ml[k](1) - ml[k](0) + for d := 1; d < degGJ; d++ { + mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + } + } + + eIndex := 0 // index for where the current eq term is + nextEIndex := len(ml) + for d := range degGJ { + for i := range gateInput { + gateInput[i] = &mlEvals[eIndex+1+i] + } + summand := wire.Gate.Evaluate(api, gateInput...).(*{{ .ElementType }}) + summand.Mul(summand, &mlEvals[eIndex]) + res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + } + } + mu.Lock() + for i := range gJ { + gJ[i].Add(&gJ[i], &res[i]) // collect into the complete sum + } + mu.Unlock() + } + + const minBlockSize = 64 + + if sumSize < minBlockSize { + // no parallelization + computeAll(0, sumSize) + } else { + c.manager.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } + + return gJ +} + +// next first folds the input and E polynomials at the given verifier challenge then computes the new gⱼ. +// Thus, j <- j+1 and rⱼ = challenge. +func (c *eqTimesGateEvalSumcheckClaims) next(challenge {{ .ElementType }}) polynomial.Polynomial { + const minBlockSize = 512 + n := len(c.eq) / 2 + if n < minBlockSize { + // no parallelization + for i := 0; i < len(c.input); i++ { + c.input[i].Fold(challenge) + } + c.eq.Fold(challenge) + } else { + wgs := make([]*sync.WaitGroup, len(c.input)) + for i := 0; i < len(c.input); i++ { + wgs[i] = c.manager.workers.Submit(n, c.input[i].FoldParallel(challenge), minBlockSize) + } + c.manager.workers.Submit(n, c.eq.FoldParallel(challenge), minBlockSize).Wait() + for _, wg := range wgs { + wg.Wait() + } + } + + return c.computeGJ() +} + +func (c *eqTimesGateEvalSumcheckClaims) varsNum() int { + return len(c.evaluationPoints[0]) +} + +func (c *eqTimesGateEvalSumcheckClaims) claimsNum() int { + return len(c.claimedEvaluations) +} + +// proveFinalEval provides the values wᵢ(r₁, ..., rₙ) +func (c *eqTimesGateEvalSumcheckClaims) proveFinalEval(r []{{ .ElementType }}) []{{ .ElementType }} { + //defer the proof, return list of claims + + injection, _ := c.manager.wires.ClaimPropagationInfo(c.wireI) // TODO @Tabaie: Instead of doing this last, we could just have fewer input in the first place; not that likely to happen with single gates, but more so with layers. + evaluations := make([]{{ .ElementType }}, len(injection)) + for i, gateInputI := range injection { + wI := c.input[gateInputI] + wI.Fold(r[len(r)-1]) // We already have wᵢ(r₁, ..., rₙ₋₁, hₙ) in a table. Only one more fold required. + c.manager.add(c.getWire().Inputs[gateInputI], r, wI[0]) + evaluations[i] = wI[0] + } + + c.manager.memPool.Dump(c.claimedEvaluations, c.eq) + + return evaluations +} + +type claimsManager struct { + claims []*eqTimesGateEvalSumcheckLazyClaims + assignment WireAssignment + memPool *polynomial.Pool + workers *utils.WorkerPool + wires gkrtypes.Wires +} + +func newClaimsManager(wires []*gkrtypes.Wire, assignment WireAssignment, o settings) (manager claimsManager) { + manager.assignment = assignment + manager.claims = make([]*eqTimesGateEvalSumcheckLazyClaims, len(wires)) + manager.memPool = o.pool + manager.workers = o.workers + manager.wires = wires + + for i, wire := range wires { + + manager.claims[i] = &eqTimesGateEvalSumcheckLazyClaims{ + wireI: i, + evaluationPoints: make([][]{{ .ElementType }}, 0, wire.NbClaims()), + claimedEvaluations: manager.memPool.Make(wire.NbClaims()), + manager: &manager, + } + } + return +} + +func (m *claimsManager) add(wire int, evaluationPoint []{{ .ElementType }}, evaluation {{ .ElementType }}) { + claim := m.claims[wire] + i := len(claim.evaluationPoints) + claim.claimedEvaluations[i] = evaluation + claim.evaluationPoints = append(claim.evaluationPoints, evaluationPoint) +} + +func (m *claimsManager) getLazyClaim(wire int) *eqTimesGateEvalSumcheckLazyClaims { + return m.claims[wire] +} + +func (m *claimsManager) getClaim(wireI int) *eqTimesGateEvalSumcheckClaims { + lazy := m.claims[wireI] + wire := m.wires[wireI] + res := &eqTimesGateEvalSumcheckClaims{ + wireI: wireI, + evaluationPoints: lazy.evaluationPoints, + claimedEvaluations: lazy.claimedEvaluations, + manager: m, + } + + if wire.IsInput() { + res.input = []polynomial.MultiLin{m.memPool.Clone(m.assignment[wireI])} + } else { + res.input = make([]polynomial.MultiLin, len(wire.Inputs)) + + for inputI, inputW := range wire.Inputs { + res.input[inputI] = m.memPool.Clone(m.assignment[inputW]) //will be edited later, so must be deep copied + } + } + return res +} + +func (m *claimsManager) deleteClaim(wire int) { + m.claims[wire].manager = nil + m.claims[wire] = nil +} + +type settings struct { + pool *polynomial.Pool + sorted []*gkrtypes.Wire + transcript *fiatshamir.Transcript + transcriptPrefix string + nbVars int + workers *utils.WorkerPool +} + +type Option func(*settings) + +func WithPool(pool *polynomial.Pool) Option { + return func (options *settings) { + options.pool = pool + } +} + +func WithSortedCircuit(sorted []*gkrtypes.Wire) Option { + return func(options *settings) { + options.sorted = sorted + } +} + +func WithWorkers(workers *utils.WorkerPool) Option { + return func(options *settings) { + options.workers = workers + } +} + + +func setup(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (settings, error) { + var o settings + var err error + for _, option := range options { + option(&o) + } + + o.nbVars = assignment.NumVars() + nbInstances := assignment.NumInstances() + if 1< 1 { //combine the claims + size++ + } + size += logNbInstances // full run of sumcheck on logNbInstances variables + } + + nums := make([]string, max(len(sorted), logNbInstances)) + for i := range nums { + nums[i] = strconv.Itoa(i) + } + + challenges := make([]string, size) + + // output wire claims + firstChallengePrefix := prefix + "fC." + for j := 0; j < logNbInstances; j++ { + challenges[j] = firstChallengePrefix + nums[j] + } + j := logNbInstances + for i := len(sorted) - 1; i >= 0; i-- { + if sorted[i].NoProof() { + continue + } + wirePrefix := prefix + "w" + nums[i] + "." + + if sorted[i].NbClaims() > 1 { + challenges[j] = wirePrefix + "comb" + j++ + } + + partialSumPrefix := wirePrefix + "pSP." + for k := 0; k < logNbInstances; k++ { + challenges[j] = partialSumPrefix + nums[k] + j++ + } + } + return challenges +} + +func getFirstChallengeNames(logNbInstances int, prefix string) []string { + res := make([]string, logNbInstances) + firstChallengePrefix := prefix + "fC." + for i := 0; i < logNbInstances; i++ { + res[i] = firstChallengePrefix + strconv.Itoa(i) + } + return res +} + +func getChallenges(transcript *fiatshamir.Transcript, names []string) ([]{{ .ElementType }}, error) { + res := make([]{{ .ElementType }}, len(names)) + for i, name := range names { + if bytes, err := transcript.ComputeChallenge(name); err == nil { + res[i].SetBytes(bytes) + } else { + return nil, err + } + } + return res, nil +} + +// Prove consistency of the claimed assignment +func Prove(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (Proof, error) { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return nil, err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + proof := make(Proof, len(c)) + // firstChallenge called rho in the paper + var firstChallenge []{{ .ElementType }} + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return nil, err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + claim := claims.getClaim(i) + if wire.NoProof() { // input wires with one claim only + proof[i] = sumcheckProof{ + partialSumPolys: []polynomial.Polynomial{}, + finalEvalProof: []{{ .ElementType }}{}, + } + } else { + if proof[i], err = sumcheckProve( + claim, fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err != nil { + return proof, err + } + + baseChallenge = make([][]byte, len(proof[i].finalEvalProof)) + for j := range proof[i].finalEvalProof { + baseChallenge[j] = proof[i].finalEvalProof[j].Marshal() + } + } + // the verifier checks a single claim about input wires itself + claims.deleteClaim(i) + } + + return proof, nil +} + +// Verify the consistency of the claimed output with the claimed input +// Unlike in Prove, the assignment argument need not be complete +func Verify(c gkrtypes.Circuit, assignment WireAssignment, proof Proof, transcriptSettings fiatshamir.Settings, options ...Option) error { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + var firstChallenge []{{ .ElementType }} + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + proofW := proof[i] + claim := claims.getLazyClaim(i) + if wire.NoProof() { // input wires with one claim only + // make sure the proof is empty + if len(proofW.finalEvalProof) != 0 || len(proofW.partialSumPolys) != 0 { + return errors.New("no proof allowed for input wire with a single claim") + } + + if wire.NbClaims() == 1 { // input wire + // simply evaluate and see if it matches + evaluation := assignment[i].Evaluate(claim.evaluationPoints[0], claims.memPool) + if !claim.claimedEvaluations[0].Equal(&evaluation) { + return errors.New("incorrect input wire claim") + } + } + } else if err = sumcheckVerify( + claim, proof[i], fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err == nil { // incorporate prover claims about w's input into the transcript + baseChallenge = make([][]byte, len(proofW.finalEvalProof)) + for j := range baseChallenge { + baseChallenge[j] = proofW.finalEvalProof[j].Marshal() + } + } else { + return fmt.Errorf("sumcheck proof rejected: %v", err) //TODO: Any polynomials to dump? + } + claims.deleteClaim(i) + } + return nil +} + +// Complete the circuit evaluation from input values +func (a WireAssignment) Complete(wires gkrtypes.Wires) WireAssignment { + + nbInstances := a.NumInstances() + maxNbIns := 0 + + for i, w := range wires { + maxNbIns = max(maxNbIns, len(w.Inputs)) + if len(a[i]) != nbInstances { + a[i] = make([]{{ .ElementType }}, nbInstances) + } + } + + ins := make([]{{ .ElementType }}, maxNbIns) + for i := range nbInstances { + for wI, w := range wires { + if !w.IsInput() { + for inI, in := range w.Inputs { + ins[inI] = a[in][i] + } + a[wI][i].Set(api.evaluate(w.Gate.Evaluate, ins[:len(w.Inputs)]...)) + } + } + } + + return a +} + +func (a WireAssignment) NumInstances() int { + for _, aW := range a { + return len(aW) + } + panic("empty assignment") +} + +func (a WireAssignment) NumVars() int { + for _, aW := range a { + return aW.NumVars() + } + panic("empty assignment") +} + +// SerializeToBigInts flattens a proof object into the given slice of big.Ints +// useful in gnark hints. +func (p Proof) SerializeToBigInts(outs []*big.Int) error { + offset := 0 + for i := range p { + for _, poly := range p[i].partialSumPolys { + frToBigInts(outs[offset:], poly) + offset += len(poly) + } + if p[i].finalEvalProof != nil { + frToBigInts(outs[offset:], p[i].finalEvalProof) + offset += len(p[i].finalEvalProof) + } + } + if offset != len(outs) { + return fmt.Errorf("expected %d elements, got %d", offset, len(outs)) + } + return nil +} + +func frToBigInts(dst []*big.Int, src []{{ .ElementType }}) { + for i := range src { + src[i].BigInt(dst[i]) + } +} + + +// gateAPI implements gkr.GateAPI. +type gateAPI struct{} + +var api gateAPI + +func (gateAPI) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res {{ .ElementType }} // TODO Heap allocated. Keep an eye on perf + res.Add(cast(i1), cast(i2)) + for _, v := range in { + res.Add(&res, cast(v)) + } + return &res +} + +func (gateAPI) MulAcc(a, b, c frontend.Variable) frontend.Variable { + var prod {{ .ElementType }} + prod.Add(cast(b), cast(c)) + res := cast(a) + res.Add(res, &prod) + return &res +} + +func (gateAPI) Neg(i1 frontend.Variable) frontend.Variable { + var res {{ .ElementType }} + res.Neg(cast(i1)) + return &res +} + +func (gateAPI) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res {{ .ElementType }} + res.Sub(cast(i1), cast(i2)) + for _, v := range in { + res.Sub(&res, cast(v)) + } + return &res +} + +func (gateAPI) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res {{ .ElementType }} + res.Mul(cast(i1), cast(i2)) + for _, v := range in { + res.Mul(&res, cast(v)) + } + return &res +} + +func (gateAPI) Println(a ...frontend.Variable) { + toPrint := make([]any, len(a)) + var x {{ .ElementType }} + + for i, v := range a { + if _, err := x.SetInterface(v); err != nil { + toPrint[i] = x.String() + } else { + if s, ok := v.(string); ok { + toPrint[i] = s + continue + } + panic(fmt.Errorf("not numeric or string: %w", err)) + } + } + fmt.Println(toPrint...) +} + +func (api gateAPI) evaluate(f gkr.GateFunction, in ...{{ .ElementType }}) *{{ .ElementType }} { + inVar := make([]frontend.Variable, len(in)) + for i := range in { + inVar[i] = &in[i] + } + return f(api, inVar...).(*{{ .ElementType }}) +} + +type gateFunctionFr func(...{{ .ElementType }}) *{{ .ElementType }} + +// convertFunc turns f into a function that accepts and returns {{ .ElementType }}. +func (api gateAPI) convertFunc(f gkr.GateFunction) gateFunctionFr { + return func(in ...{{ .ElementType }}) *{{ .ElementType }} { + return api.evaluate(f, in...) + } +} + +func cast(v frontend.Variable) *{{ .ElementType }} { + if x, ok := v.(*{{ .ElementType }}); ok { // fast path, no extra heap allocation + return x + } + var x {{ .ElementType }} + if _, err := x.SetInterface(v); err != nil { + panic(err) + } + return &x +} \ No newline at end of file diff --git a/internal/generator/backend/template/gkr/gkr.test.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.go.tmpl new file mode 100644 index 00000000..afeaabf9 --- /dev/null +++ b/internal/generator/backend/template/gkr/gkr.test.go.tmpl @@ -0,0 +1,378 @@ + +import ( + "{{.FieldPackagePath}}" + "{{.FieldPackagePath}}/mimc" + "{{.FieldPackagePath}}/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + gcUtils "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/internal/utils" + "github.com/consensys/gnark/std/gkrapi/gkr" + "github.com/stretchr/testify/assert" + "fmt" + "hash" + "os" + "strconv" + "testing" + "path/filepath" + "encoding/json" + "reflect" + "time" +) + +func TestNoGateTwoInstances(t *testing.T) { + // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case + testNoGate(t, []{{ .ElementType }}{four, three}) +} + +func TestNoGate(t *testing.T) { + test(t, gkrtypes.Circuit{ {} }) +} + +func TestSingleAddGate(t *testing.T) { + test(t, gkrtypes.Circuit{ {}, {}, { + Gate: gkrtypes.Add2(), + Inputs: []int{0, 1}, + } }) +} + +func TestSingleMulGate(t *testing.T) { + test(t, gkrtypes.Circuit{ {}, {}, { + Gate: gkrtypes.Mul2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleInputTwoIdentityGates(t *testing.T) { + test(t, gkrtypes.Circuit{ {}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + }) +} + +func TestSingleInputTwoIdentityGatesComposed(t *testing.T) { + test(t, gkrtypes.Circuit{ {}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{1}, + }}) +} + +func TestAPowNTimesBCircuit(t *testing.T) { + const N = 10 + + c := make(gkrtypes.Circuit, N+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: gkrtypes.Mul2(), + Inputs: []int{i - 1, 0}, + } + } + + test(t, c) +} + +func TestSingleMimcCipherGate(t *testing.T) { + test(t, gkrtypes.Circuit{ + {}, {}, + { + Inputs: []int{0, 1}, + Gate: cache.GetGate("mimc"), + }, + }) +} + +func TestShallowMimcTwoInstances(t *testing.T) { + test(t, mimcCircuit(2)) +} + +func TestMimc(t *testing.T) { + test(t, mimcCircuit(93)) +} + +func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { + circuit := gkrtypes.Circuit{gkrtypes.Wire{ + Gate: gkrtypes.Identity(), + NbUniqueOutputs: 2, + }} + + assignment := WireAssignment{[]{{ .ElementType }}{two, three}} + var o settings + pool := polynomial.NewPool(256, 1<<11) + workers := gcUtils.NewWorkerPool() + o.pool = &pool + o.workers = workers + + claimsManagerGen := func() *claimsManager { + manager := newClaimsManager(utils.References(circuit), assignment, o) + manager.add(0, []{{ .ElementType }}{three}, five) + manager.add(0, []{{ .ElementType }}{four}, six) + return &manager + } + + transcriptGen := newMessageCounterGenerator(4, 1) + + proof, err := sumcheckProve(claimsManagerGen().getClaim(0), fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) + err = sumcheckVerify(claimsManagerGen().getLazyClaim(0), proof, fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) +} + +var one, two, three, four, five, six {{ .ElementType }} + +func init() { + one.SetOne() + two.Double(&one) + three.Add(&two, &one) + four.Double(&two) + five.Add(&three, &two) + six.Double(&three) +} + +var testManyInstancesLogMaxInstances = -1 + +func getLogMaxInstances(t *testing.T) int { + if testManyInstancesLogMaxInstances == -1 { + + s := os.Getenv("GKR_LOG_INSTANCES") + if s == "" { + testManyInstancesLogMaxInstances = 5 + } else { + var err error + testManyInstancesLogMaxInstances, err = strconv.Atoi(s) + if err != nil { + t.Error(err) + } + } + + } + return testManyInstancesLogMaxInstances +} + +func test(t *testing.T, circuit gkrtypes.Circuit) { + wireRefs := utils.References(circuit) + ins := circuit.Inputs() + insAssignment := make(WireAssignment, len(ins)) + maxSize := 1 << getLogMaxInstances(t) + + for i := range ins { + insAssignment[i] = make([]{{ .ElementType }}, maxSize) + fr.Vector(insAssignment[i]).MustSetRandom() + } + + fullAssignment := make(WireAssignment, len(circuit)) + for _, numEvals := range []int{2, maxSize} { + for i := range ins { + fullAssignment[ins[i]] = insAssignment[i][:numEvals] + } + + fullAssignment.Complete(wireRefs) + + t.Log("Selected inputs for test") + + proof, err := Prove(circuit, fullAssignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") + + if proof.isEmpty() { // special case for TestNoGate: + continue // there's no way to make a trivial proof fail + } + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(0, 1))) + assert.NotNil(t, err, "bad proof accepted") + } + +} + +func (p Proof) isEmpty() bool { + for i := range p { + if len(p[i].finalEvalProof) != 0 { + return false + } + for j := range p[i].partialSumPolys { + if len(p[i].partialSumPolys[j]) != 0 { + return false + } + } + } + return true +} + +func testNoGate(t *testing.T, inputAssignments ...[]{{ .ElementType }}) { + c := gkrtypes.Circuit{ + {}, + } + + assignment := WireAssignment{0: inputAssignments[0]} + + proof, err := Prove(c, assignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(c, assignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") +} + +func mimcCircuit(numRounds int) gkrtypes.Circuit { + c := make(gkrtypes.Circuit, numRounds+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: cache.GetGate("mimc"), + Inputs: []int{i - 1, 0}, + } + } + return c +} + + +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("bivariate input needed") + } + res := api.Add(x[0], x[1]) + return api.Mul(res, x[0]) + } + + // g: x,y -> x² + 3y + g := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + res := api.Mul(x[0], x[0]) + y3 := api.Mul(x[1], 3) + return api.Add(res, y3) + } + + // h: x -> 2x + // but it edits it input + h := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + return api.Add(x[0], x[0]) + } + + assert.False(t, IsGateFunctionAdditive(f, 1, 2)) + assert.False(t, IsGateFunctionAdditive(f, 0, 2)) + + assert.False(t, IsGateFunctionAdditive(g, 0, 2)) + assert.True(t, IsGateFunctionAdditive(g, 1, 2)) + + assert.True(t, IsGateFunctionAdditive(h, 0, 1)) +} + +func generateTestProver(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + proof, err := Prove(testCase.Circuit, testCase.FullAssignment, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err) + assert.NoError(t, proofEquals(testCase.Proof, proof)) + } +} + +func generateTestVerifier(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err, "proof rejected") + testCase, err = newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(newMessageCounter(2, 0))) + assert.NotNil(t, err, "bad proof accepted") + } +} + +func TestGkrVectors(t *testing.T) { + + const testDirPath = "../test_vectors/" + dirEntries, err := os.ReadDir(testDirPath) + assert.NoError(t, err) + for _, dirEntry := range dirEntries { + if !dirEntry.IsDir() { + + if filepath.Ext(dirEntry.Name()) == ".json" { + path := filepath.Join(testDirPath, dirEntry.Name()) + noExt := dirEntry.Name()[:len(dirEntry.Name())-len(".json")] + + t.Run(noExt+"_prover", generateTestProver(path)) + t.Run(noExt+"_verifier", generateTestVerifier(path)) + + } + } + } +} + +func proofEquals(expected Proof, seen Proof) error { + if len(expected) != len(seen) { + return fmt.Errorf("length mismatch %d ≠ %d", len(expected), len(seen)) + } + for i, x := range expected { + xSeen := seen[i] + + if xSeen.finalEvalProof == nil { + if seenFinalEval := x.finalEvalProof; len(seenFinalEval) != 0 { + return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) + } + } else { + if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + return fmt.Errorf("final evaluation proof mismatch") + } + } + if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + return err + } + } + return nil +} + +func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { + fmt.Println("creating circuit structure") + c := mimcCircuit(mimcDepth) + + in0 := make([]{{ .ElementType }}, nbInstances) + in1 := make([]{{ .ElementType }}, nbInstances) + {{.FieldPackageName}}.Vector(in0).MustSetRandom() + {{.FieldPackageName}}.Vector(in1).MustSetRandom() + + fmt.Println("evaluating circuit") + start := time.Now().UnixMicro() + assignment := WireAssignment{in0, in1}.Complete(utils.References(c)) + solved := time.Now().UnixMicro() - start + fmt.Println("solved in", solved, "μs") + + //b.ResetTimer() + fmt.Println("constructing proof") + start = time.Now().UnixMicro() + _, err := Prove(c, assignment, fiatshamir.WithHash(mimc.NewMiMC())) + proved := time.Now().UnixMicro() - start + fmt.Println("proved in", proved, "μs") + assert.NoError(b, err) +} + +func BenchmarkGkrMimc19(b *testing.B) { + benchmarkGkrMiMC(b, 1<<19, 91) +} + +func BenchmarkGkrMimc17(b *testing.B) { + benchmarkGkrMiMC(b, 1<<17, 91) +} + +{{template "gkrTestVectors" .}} \ No newline at end of file diff --git a/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl new file mode 100644 index 00000000..2d305103 --- /dev/null +++ b/internal/generator/backend/template/gkr/gkr.test.vectors.gen.go.tmpl @@ -0,0 +1,151 @@ +import ( + "encoding/json" + "fmt" + "github.com/consensys/bavard" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark/internal/small_rational" + "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/internal/utils" + "hash" + "os" + "path/filepath" + "reflect" +) + +func GenerateVectors() error { + testDirPath, err := filepath.Abs("../../gkr/test_vectors") + if err != nil { + return err + } + + fmt.Printf("generating GKR test cases: scanning directory %s for test specs\n", testDirPath) + + dirEntries, err := os.ReadDir(testDirPath) + if err != nil { + return err + } + for _, dirEntry := range dirEntries { + if !dirEntry.IsDir() { + + if filepath.Ext(dirEntry.Name()) == ".json" { + path := filepath.Join(testDirPath, dirEntry.Name()) + if !bavard.ShouldGenerate(path) { + continue + } + fmt.Println("\tprocessing", dirEntry.Name()) + if err = run(path); err != nil { + return err + } + } + } + } + + return nil +} + +func run(absPath string) error { + testCase, err := newTestCase(absPath) + if err != nil { + return err + } + + transcriptSetting := fiatshamir.WithHash(testCase.Hash) + + var proof Proof + proof, err = Prove(testCase.Circuit, testCase.FullAssignment, transcriptSetting) + if err != nil { + return err + } + + if testCase.Info.Proof, err = toPrintableProof(proof); err != nil { + return err + } + var outBytes []byte + if outBytes, err = json.MarshalIndent(testCase.Info, "", "\t"); err == nil { + if err = os.WriteFile(absPath, outBytes, 0); err != nil { + return err + } + } else { + return err + } + + testCase, err = newTestCase(absPath) + if err != nil { + return err + } + + err = Verify(testCase.Circuit, testCase.InOutAssignment, proof, transcriptSetting) + if err != nil { + return err + } + + testCase, err = newTestCase(absPath) + if err != nil { + return err + } + + err = Verify(testCase.Circuit, testCase.InOutAssignment, proof, fiatshamir.WithHash(newMessageCounter(2, 0))) + if err == nil { + return fmt.Errorf("bad proof accepted") + } + return nil +} + +func toPrintableProof(proof Proof) (gkrtesting.PrintableProof, error) { + res := make(gkrtesting.PrintableProof, len(proof)) + + for i := range proof { + + partialSumPolys := make([][]interface{}, len(proof[i].partialSumPolys)) + for k, partialK := range proof[i].partialSumPolys { + partialSumPolys[k] = elementSliceToInterfaceSlice(partialK) + } + + res[i] = gkrtesting.PrintableSumcheckProof{ + FinalEvalProof: elementSliceToInterfaceSlice(proof[i].finalEvalProof), + PartialSumPolys: partialSumPolys, + } + } + return res, nil +} + +func elementToInterface(x *{{.ElementType}}) interface{} { + if i := x.BigInt(nil); i != nil { + return i + } + return x.Text(10) +} + +func elementSliceToInterfaceSlice(x interface{}) []interface{} { + if x == nil { + return nil + } + + X := reflect.ValueOf(x) + + res := make([]interface{}, X.Len()) + for i := range res { + xI := X.Index(i).Interface().({{.ElementType}}) + res[i] = elementToInterface(&xI) + } + return res +} + +func elementSliceSliceToInterfaceSliceSlice(x interface{}) [][]interface{} { + if x == nil { + return nil + } + + X := reflect.ValueOf(x) + + res := make([][]interface{}, X.Len()) + for i := range res { + res[i] = elementSliceToInterfaceSlice(X.Index(i).Interface()) + } + + return res +} + +{{template "gkrTestVectors" .}} \ No newline at end of file diff --git a/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl new file mode 100644 index 00000000..9d138b67 --- /dev/null +++ b/internal/generator/backend/template/gkr/gkr.test.vectors.go.tmpl @@ -0,0 +1,139 @@ +{{define "gkrTestVectors"}} + +{{$CheckOutputCorrectness := true}} + +func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { + proof := make(Proof, len(printable)) + for i := range printable { + finalEvalProof := []{{.ElementType}}(nil) + + if printable[i].FinalEvalProof != nil { + finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) + finalEvalProof = make([]{{.ElementType}}, finalEvalSlice.Len()) + for k := range finalEvalProof { + if _, err := {{ setElement "finalEvalProof[k]" "finalEvalSlice.Index(k).Interface()" .ElementType}}; err != nil { + return nil, err + } + } + } + + proof[i] = sumcheckProof{ + partialSumPolys: make([]polynomial.Polynomial, len(printable[i].PartialSumPolys)), + finalEvalProof: finalEvalProof, + } + for k := range printable[i].PartialSumPolys { + var err error + if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + return nil, err + } + } + } + return proof, nil +} + +type TestCase struct { + Circuit gkrtypes.Circuit + Hash hash.Hash + Proof Proof + FullAssignment WireAssignment + InOutAssignment WireAssignment + {{if .GenerateTestVectors}}Info gkrtesting.TestCaseInfo // we are generating the test vectors, so we need to keep the circuit instance info to ADD the proof to it and resave it{{end}} +} + +var ( + testCases = make(map[string]*TestCase) + cache = gkrtesting.NewCache() +) + +func newTestCase(path string) (*TestCase, error) { + path, err := filepath.Abs(path) + if err != nil { + return nil, err + } + dir := filepath.Dir(path) + + tCase, ok := testCases[path] + if ok { + return tCase, nil + } + + info, err := cache.ReadTestCaseInfo(path) + if err != nil { + return nil, err + } + + circuit := cache.GetCircuit(filepath.Join(dir, info.Circuit)) + var _hash hash.Hash + if _hash, err = hashFromDescription(info.Hash); err != nil { + return nil, err + } + var proof Proof + if proof, err = unmarshalProof(info.Proof); err != nil { + return nil, err + } + + fullAssignment := make(WireAssignment, len(circuit)) + inOutAssignment := make(WireAssignment, len(circuit)) + + sorted := circuit.TopologicalSort() + + inI, outI := 0, 0 + for i, w := range sorted { + var assignmentRaw []interface{} + if w.IsInput() { + if inI == len(info.Input) { + return nil, fmt.Errorf("fewer input in vector than in circuit") + } + assignmentRaw = info.Input[inI] + inI++ + } else if w.IsOutput() { + if outI == len(info.Output) { + return nil, fmt.Errorf("fewer output in vector than in circuit") + } + assignmentRaw = info.Output[outI] + outI++ + } + if assignmentRaw != nil { + var wireAssignment []{{ .ElementType }} + if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + return nil, err + } + + fullAssignment[i] = wireAssignment + inOutAssignment[i] = wireAssignment + } + } + + fullAssignment.Complete(utils.References(circuit)) + + for i, w := range sorted { + if w.IsOutput() { + if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + return nil, fmt.Errorf("assignment mismatch: %v", err) + } + } + } + + tCase = &TestCase{ + FullAssignment: fullAssignment, + InOutAssignment: inOutAssignment, + Proof: proof, + Hash: _hash, + Circuit: circuit, + {{ if .GenerateTestVectors }} Info: info, {{ end }} + } + + testCases[path] = tCase + + return tCase, nil +} + +{{end}} + +{{- define "setElement element value elementType"}} +{{- if eq .elementType "fr.Element"}} setElement(&{{.element}}, {{.value}}) +{{- else if eq .elementType "small_rational.SmallRational"}} {{.element}}.SetInterface({{.value}}) +{{- else}} +{{print "\"UNEXPECTED TYPE" .elementType "\""}} +{{- end}} +{{- end}} \ No newline at end of file diff --git a/internal/generator/backend/template/gkr/solver_hints.go.tmpl b/internal/generator/backend/template/gkr/solver_hints.go.tmpl new file mode 100644 index 00000000..e1d41e8c --- /dev/null +++ b/internal/generator/backend/template/gkr/solver_hints.go.tmpl @@ -0,0 +1,139 @@ +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark-crypto/hash" + "github.com/consensys/gnark-crypto/utils" + hint "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + algo_utils "github.com/consensys/gnark/internal/utils" + + "{{ .FieldPackagePath }}" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +type SolvingData struct { + assignment WireAssignment + circuit gkrtypes.Circuit + workers *utils.WorkerPool +} + +func (d *SolvingData) init(info gkrtypes.SolvingInfo) { + d.workers = utils.NewWorkerPool() + d.circuit = info.Circuit + d.circuit.SetNbUniqueOutputs() + + d.assignment = make(WireAssignment, len(d.circuit)) + for i := range d.assignment { + d.assignment[i] = make([]fr.Element, info.NbInstances) + } +} + +// this module assumes that wire and instance indexes respect dependencies + +func setOuts(a WireAssignment, circuit gkrtypes.Circuit, outs []*big.Int) { + outsI := 0 + for i := range circuit { + if circuit[i].IsOutput() { + for j := range a[i] { + a[i][j].BigInt(outs[outsI]) + outsI++ + } + } + } + // Check if outsI == len(outs)? +} + +func SolveHint(info gkrtypes.SolvingInfo, data *SolvingData) hint.Hint { + return func(_ *big.Int, ins, outs []*big.Int) error { + // assumes assignmentVector is arranged wire first, instance second in order of solution + offsets := info.AssignmentOffsets() + data.init(info) + maxNIn := data.circuit.MaxGateNbIn() + + chunks := info.Chunks() + + solveTask := func(chunkOffset int) utils.Task { + return func(startInChunk, endInChunk int) { + start := startInChunk + chunkOffset + end := endInChunk + chunkOffset + inputs := make([]frontend.Variable, maxNIn) + dependencyHeads := make([]int, len(data.circuit)) // for each wire, which of its dependencies we would look at next + for wI := range data.circuit { // skip instances that are not relevant (related to instances before the current task) + deps := info.Dependencies[wI] + dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { + return deps[i].InputInstance + }, len(deps), start) + } + + for instanceI := start; instanceI < end; instanceI++ { + for wireI := range data.circuit { + wire := &data.circuit[wireI] + deps := info.Dependencies[wireI] + if wire.IsInput() { + if dependencyHeads[wireI] < len(deps) && instanceI == deps[dependencyHeads[wireI]].InputInstance { + dep := deps[dependencyHeads[wireI]] + data.assignment[wireI][instanceI].Set(&data.assignment[dep.OutputWire][dep.OutputInstance]) + dependencyHeads[wireI]++ + } else { + data.assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) + } + } else { + // assemble the inputs + inputIndexes := info.Circuit[wireI].Inputs + for i, inputI := range inputIndexes { + inputs[i] = &data.assignment[inputI][instanceI] + } + gate := data.circuit[wireI].Gate + data.assignment[wireI][instanceI].Set(gate.Evaluate(api, inputs[:len(inputIndexes)]...).(*fr.Element)) + } + } + } + } + } + + start := 0 + for _, end := range chunks { + data.workers.Submit(end-start, solveTask(start), 1024).Wait() + start = end + } + + for _, p := range info.Prints { + serializable := make([]any, len(p.Values)) + for i, v := range p.Values { + if p.IsGkrVar[i] { // serializer stores uint32 in slices as uint64 + serializable[i] = data.assignment[algo_utils.ForceUint32(v)][p.Instance].String() + } else { + serializable[i] = v + } + } + fmt.Println(serializable...) + } + + setOuts(data.assignment, info.Circuit, outs) + + return nil + } +} + +func ProveHint(hashName string, data *SolvingData) hint.Hint { + + return func(_ *big.Int, ins, outs []*big.Int) error { + insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called + b := make([]byte, fr.Bytes) + i.FillBytes(b) + return b[:] + }) + + hsh := hash.NewHash(hashName + "_{{.FieldID}}") + + proof, err := Prove(data.circuit, data.assignment, fiatshamir.WithHash(hsh, insBytes...), WithWorkers(data.workers)) + if err != nil { + return err + } + + return proof.SerializeToBigInts(outs) + + } +} \ No newline at end of file diff --git a/internal/generator/backend/template/gkr/sumcheck.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.go.tmpl new file mode 100644 index 00000000..7a351fc2 --- /dev/null +++ b/internal/generator/backend/template/gkr/sumcheck.go.tmpl @@ -0,0 +1,163 @@ +import ( + "errors" + "{{.FieldPackagePath}}" + "{{.FieldPackagePath}}/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "strconv" +) + +// This does not make use of parallelism and represents polynomials as lists of coefficients +// It is currently geared towards arithmetic hashes. Once we have a more unified hash function interface, this can be generified. + +// sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. +// Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) +type sumcheckClaims interface { + combine(a {{.ElementType}}) polynomial.Polynomial // combine into the 0ᵗʰ sumcheck subclaim. Create g := ∑_{1≤j≤m} aʲ⁻¹fⱼ for which now we seek to prove ∑_{0≤i<2ⁿ} g(i) = c := ∑_{1≤j≤m} aʲ⁻¹cⱼ. Return g₁. + next({{.ElementType}}) polynomial.Polynomial // Return the evaluations gⱼ(k) for 1 ≤ k < degⱼ(g). Update the claim to gⱼ₊₁ for the input value as rⱼ + varsNum() int // number of variables + claimsNum() int // number of claims + proveFinalEval(r []{{.ElementType}}) []{{.ElementType}} // in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +// sumcheckLazyClaims is the sumcheckClaims data structure on the verifier side. It is "lazy" in that it has to compute fewer things. +type sumcheckLazyClaims interface { + claimsNum() int // claimsNum = m + varsNum() int // varsNum = n + combinedSum(a {{.ElementType}}) {{.ElementType}} // combinedSum returns c = ∑_{1≤j≤m} aʲ⁻¹cⱼ + degree(i int) int // degree of the total claim in the i'th variable + verifyFinalEval(r []{{.ElementType}}, combinationCoeff {{.ElementType}}, purportedValue {{.ElementType}}, proof []{{.ElementType}}) error +} + +// sumcheckProof of a multi-statement. +type sumcheckProof struct { + partialSumPolys []polynomial.Polynomial + finalEvalProof []{{.ElementType}} //in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +func setupTranscript(claimsNum int, varsNum int, settings *fiatshamir.Settings) (challengeNames []string, err error) { + numChallenges := varsNum + if claimsNum >= 2 { + numChallenges++ + } + challengeNames = make([]string, numChallenges) + if claimsNum >= 2 { + challengeNames[0] = settings.Prefix + "comb" + } + prefix := settings.Prefix + "pSP." + for i := 0; i < varsNum; i++ { + challengeNames[i+numChallenges-varsNum] = prefix + strconv.Itoa(i) + } + if settings.Transcript == nil { + transcript := fiatshamir.NewTranscript(settings.Hash, challengeNames...) + settings.Transcript = transcript + } + + for i := range settings.BaseChallenges { + if err = settings.Transcript.Bind(challengeNames[0], settings.BaseChallenges[i]); err != nil { + return + } + } + return +} + +func next(transcript *fiatshamir.Transcript, bindings []{{.ElementType}}, remainingChallengeNames *[]string) ({{.ElementType}}, error) { + challengeName := (*remainingChallengeNames)[0] + for i := range bindings { + bytes := bindings[i].Bytes() + if err := transcript.Bind(challengeName, bytes[:]); err != nil { + return {{.ElementType}}{}, err + } + } + var res {{.ElementType}} + bytes, err := transcript.ComputeChallenge(challengeName) + res.SetBytes(bytes) + + *remainingChallengeNames = (*remainingChallengeNames)[1:] + + return res, err +} + +// sumcheckProve create a non-interactive proof +func sumcheckProve(claims sumcheckClaims, transcriptSettings fiatshamir.Settings) (sumcheckProof, error) { + + var proof sumcheckProof + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return proof, err + } + + var combinationCoeff {{.ElementType}} + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []{{.ElementType}}{}, &remainingChallengeNames); err != nil { + return proof, err + } + } + + varsNum := claims.varsNum() + proof.partialSumPolys = make([]polynomial.Polynomial, varsNum) + proof.partialSumPolys[0] = claims.combine(combinationCoeff) + challenges := make([]{{.ElementType}}, varsNum) + + for j := 0; j+1 < varsNum; j++ { + if challenges[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return proof, err + } + proof.partialSumPolys[j+1] = claims.next(challenges[j]) + } + + if challenges[varsNum-1], err = next(transcript, proof.partialSumPolys[varsNum-1], &remainingChallengeNames); err != nil { + return proof, err + } + + proof.finalEvalProof = claims.proveFinalEval(challenges) + + return proof, nil +} + +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, transcriptSettings fiatshamir.Settings) error { + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return err + } + + var combinationCoeff {{.ElementType}} + + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []{{.ElementType}}{}, &remainingChallengeNames); err != nil { + return err + } + } + + r := make([]{{.ElementType}}, claims.varsNum()) + + // Just so that there is enough room for gJ to be reused + maxDegree := claims.degree(0) + for j := 1; j < claims.varsNum(); j++ { + if d := claims.degree(j); d > maxDegree { + maxDegree = d + } + } + gJ := make(polynomial.Polynomial, maxDegree+1) //At the end of iteration j, gJ = ∑_{i < 2ⁿ⁻ʲ⁻¹} g(X₁, ..., Xⱼ₊₁, i...) NOTE: n is shorthand for claims.varsNum() + gJR := claims.combinedSum(combinationCoeff) // At the beginning of iteration j, gJR = ∑_{i < 2ⁿ⁻ʲ} g(r₁, ..., rⱼ, i...) + + for j := range claims.varsNum() { + if len(proof.partialSumPolys[j]) != claims.degree(j) { + return errors.New("malformed proof") + } + copy(gJ[1:], proof.partialSumPolys[j]) + gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) // Requirement that gⱼ(0) + gⱼ(1) = gⱼ₋₁(r) + // gJ is ready + + //Prepare for the next iteration + if r[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return err + } + // This is an extremely inefficient way of interpolating. TODO: Interpolate without symbolically computing a polynomial + gJCoeffs := polynomial.InterpolateOnRange(gJ[:(claims.degree(j) + 1)]) + gJR = gJCoeffs.Eval(&r[j]) + } + + return claims.verifyFinalEval(r, combinationCoeff, gJR, proof.finalEvalProof) +} diff --git a/internal/generator/backend/template/gkr/sumcheck.test.defs.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.defs.go.tmpl new file mode 100644 index 00000000..227d0c70 --- /dev/null +++ b/internal/generator/backend/template/gkr/sumcheck.test.defs.go.tmpl @@ -0,0 +1,65 @@ +{{ define "sumcheckTestDefs" }} + +type singleMultilinClaim struct { + g polynomial.MultiLin +} + +func (c singleMultilinClaim) proveFinalEval(r []{{.ElementType}}) []{{.ElementType}} { + return nil // verifier can compute the final eval itself +} + +func (c singleMultilinClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} + +func (c singleMultilinClaim) claimsNum() int { + return 1 +} + +func sumForX1One(g polynomial.MultiLin) polynomial.Polynomial { + sum := g[len(g)/2] + for i := len(g)/2 + 1; i < len(g); i++ { + sum.Add(&sum, &g[i]) + } + return []{{.ElementType}}{sum} +} + +func (c singleMultilinClaim) combine({{.ElementType}}) polynomial.Polynomial { + return sumForX1One(c.g) +} + +func (c *singleMultilinClaim) next(r {{.ElementType}}) polynomial.Polynomial { + c.g.Fold(r) + return sumForX1One(c.g) +} + +type singleMultilinLazyClaim struct { + g polynomial.MultiLin + claimedSum {{.ElementType}} +} + +func (c singleMultilinLazyClaim) verifyFinalEval(r []{{.ElementType}}, combinationCoeff {{.ElementType}}, purportedValue {{.ElementType}}, proof []{{.ElementType}}) error { + val := c.g.Evaluate(r, nil) + if val.Equal(&purportedValue) { + return nil + } + return fmt.Errorf("mismatch") +} + +func (c singleMultilinLazyClaim) combinedSum(combinationCoeffs {{.ElementType}}) {{.ElementType}} { + return c.claimedSum +} + +func (c singleMultilinLazyClaim) degree(i int) int { + return 1 +} + +func (c singleMultilinLazyClaim) claimsNum() int { + return 1 +} + +func (c singleMultilinLazyClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} + +{{ end }} \ No newline at end of file diff --git a/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl new file mode 100644 index 00000000..72e0f763 --- /dev/null +++ b/internal/generator/backend/template/gkr/sumcheck.test.go.tmpl @@ -0,0 +1,85 @@ +import ( + "fmt" + "{{.FieldPackagePath}}/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/stretchr/testify/assert" + "hash" + {{ if not .GenerateTestVectors}} + "{{.FieldPackagePath}}" + "math/bits" + {{ end }} + "strings" + "testing" +) + +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { + poly := make(polynomial.MultiLin, len(polyInt)) + for i, n := range polyInt { + poly[i].SetUint64(n) + } + + claim := singleMultilinClaim{g: poly.Clone()} + + proof, err := sumcheckProve(&claim, fiatshamir.WithHash(hashGenerator())) + if err != nil { + return err + } + + var sb strings.Builder + for _, p := range proof.partialSumPolys { + + sb.WriteString("\t{") + for i := 0; i < len(p); i++ { + sb.WriteString(p[i].String()) + if i+1 < len(p) { + sb.WriteString(", ") + } + } + sb.WriteString("}\n") + } + + lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if err = sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())); err != nil { + return err + } + + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())) == nil { + return fmt.Errorf("bad proof accepted") + } + return nil +} + +func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { + + polys := [][]uint64{ + {1, 2, 3, 4}, // 1 + 2X₁ + X₂ + {1, 2, 3, 4, 5, 6, 7, 8}, // 1 + 4X₁ + 2X₂ + X₃ + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, // 1 + 8X₁ + 4X₂ + 2X₃ + X₄ + } + + const MaxStep = 4 + const MaxStart = 4 + hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + + for step := 0; step < MaxStep; step++ { + for startState := 0; startState < MaxStart; startState++ { + if step == 0 && startState == 1 { // unlucky case where a bad proof would be accepted + continue + } + hashGens = append(hashGens, newMessageCounterGenerator(startState, step)) + } + } + + for _, poly := range polys { + for _, hashGen := range hashGens { + assert.NoError(t, testSumcheckSingleClaimMultilin(poly, hashGen), + "failed with poly %v and hashGen %v", poly, hashGen()) + } + } +} + +{{ if not .GenerateTestVectors }} +{{ template "sumcheckTestDefs" .}} +{{ end }} \ No newline at end of file diff --git a/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl new file mode 100644 index 00000000..d47ce5bc --- /dev/null +++ b/internal/generator/backend/template/gkr/sumcheck.test.vectors.gen.go.tmpl @@ -0,0 +1,141 @@ +import ( + "encoding/json" + "fmt" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark/internal/small_rational" + "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "hash" + "math/bits" + "os" + "path/filepath" + "runtime/pprof" +) + +func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { + + var poly polynomial.MultiLin + if v, err := sliceToElementSlice(testCaseInfo.Values); err == nil { + poly = v + } else { + return err + } + + var ( + hsh hash.Hash + err error + ) + + if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { + return err + } + + proof, err := sumcheckProve( + &singleMultilinClaim{poly}, fiatshamir.WithHash(hsh)) + if err != nil { + return err + } + testCaseInfo.Proof = sumcheckToPrintableProof(proof) + + // Verification + if v, _err := sliceToElementSlice(testCaseInfo.Values); _err == nil { + poly = v + } else { + return _err + } + var claimedSum small_rational.SmallRational + if _, err = claimedSum.SetInterface(testCaseInfo.ClaimedSum); err != nil { + return err + } + + if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, fiatshamir.WithHash(hsh)); err != nil { + return fmt.Errorf("proof rejected: %v", err) + } + + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, fiatshamir.WithHash(hsh)); err == nil { + return fmt.Errorf("bad proof accepted") + } + + pprof.StopCPUProfile() + //return f.Close() + + return nil +} + +func runSumcheck(testCaseInfo *sumcheckTestCaseInfo) error { + switch testCaseInfo.Type { + case "multilin": + return runMultilin(testCaseInfo) + default: + return fmt.Errorf("type \"%s\" unrecognized", testCaseInfo.Type) + } +} + +func GenerateSumcheckVectors() error { + // read the test vectors file, generate the proof, make sure it verifies, + // and add the proof to the same file + const relPath = "../../gkr/test_vectors/sumcheck/vectors.json" + + var filename string + var err error + if filename, err = filepath.Abs(relPath); err != nil { + return err + } + + var bytes []byte + + if bytes, err = os.ReadFile(filename); err != nil { + return err + } + + var testCasesInfo sumcheckTestCasesInfo + if err = json.Unmarshal(bytes, &testCasesInfo); err != nil { + return err + } + + failed := false + for name, testCase := range testCasesInfo { + if err = runSumcheck(testCase); err != nil { + fmt.Println(name, ":", err) + failed = true + } + } + + if failed { + return fmt.Errorf("test case failed") + } + + if bytes, err = json.MarshalIndent(testCasesInfo, "", "\t"); err != nil { + return err + } + + return os.WriteFile(filename, bytes, 0) +} + +type sumcheckTestCasesInfo map[string]*sumcheckTestCaseInfo + +type sumcheckTestCaseInfo struct { + Type string `json:"type"` + Hash gkrtesting.HashDescription `json:"hash"` + Values []interface{} `json:"values"` + Description string `json:"description"` + Proof SumcheckPrintableProof `json:"proof"` + ClaimedSum interface{} `json:"claimedSum"` +} + +type SumcheckPrintableProof struct { + PartialSumPolys [][]interface{} `json:"partialSumPolys"` + FinalEvalProof interface{} `json:"finalEvalProof"` +} + +func sumcheckToPrintableProof(proof sumcheckProof) (printable SumcheckPrintableProof) { + if proof.finalEvalProof != nil { + panic("null expected") + } + printable.FinalEvalProof = struct{}{} + printable.PartialSumPolys = elementSliceSliceToInterfaceSliceSlice(proof.partialSumPolys) + return +} + +{{ template "sumcheckTestDefs" .}} \ No newline at end of file diff --git a/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl new file mode 100644 index 00000000..f5e1960a --- /dev/null +++ b/internal/generator/backend/template/gkr/test_vector_utils.go.tmpl @@ -0,0 +1,146 @@ +import ( + "fmt" + "{{.FieldPackagePath}}" + "{{.FieldPackagePath}}/polynomial" + "hash" + {{if eq .ElementType "fr.Element"}}"strings"{{- end}} + "github.com/consensys/gnark/internal/gkr/gkrtesting" +) + +func toElement(i int64) *{{.ElementType}} { + var res {{.ElementType}} + res.SetInt64(i) + return &res +} + +func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { + if _type, ok := d["type"]; ok { + switch _type { + case "const": + startState := int64(d["val"].(float64)) + return &messageCounter {startState: startState, step: 0, state: startState}, nil + default: + return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) + } + } + return nil, fmt.Errorf("hash description missing type") +} + +type messageCounter struct { + startState int64 + state int64 + step int64 +} + +func (m *messageCounter) Write(p []byte) (n int, err error) { + inputBlockSize := (len(p)-1)/{{.FieldPackageName}}.Bytes + 1 + m.state += int64(inputBlockSize) * m.step + return len(p), nil +} + +func (m *messageCounter) Sum(b []byte) []byte { + inputBlockSize := (len(b)-1)/{{.FieldPackageName}}.Bytes + 1 + resI := m.state + int64(inputBlockSize)*m.step + var res {{.ElementType}} + res.SetInt64(int64(resI)) + resBytes := res.Bytes() + return resBytes[:] +} + +func (m *messageCounter) Reset() { + m.state = m.startState +} + +func (m *messageCounter) Size() int { + return {{.FieldPackageName}}.Bytes +} + +func (m *messageCounter) BlockSize() int { + return {{.FieldPackageName}}.Bytes +} + +func newMessageCounter(startState, step int) hash.Hash { + transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} + return transcript +} + +func newMessageCounterGenerator(startState, step int) func() hash.Hash { + return func() hash.Hash { + return newMessageCounter(startState, step) + } +} + +{{ if eq .ElementType "fr.Element"}} +func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { + + // TODO: Put this in element.SetString? + switch v := value.(type) { + case string: + + if sep := strings.Split(v, "/"); len(sep) == 2 { + var denom fr.Element + if _, err := z.SetString(sep[0]); err != nil { + return nil, err + } + if _, err := denom.SetString(sep[1]); err != nil { + return nil, err + } + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil + } + + case float64: + asInt := int64(v) + if float64(asInt) != v { + return nil, fmt.Errorf("cannot currently parse float") + } + z.SetInt64(asInt) + return z, nil + } + + return z.SetInterface(value) +} +{{- end}} + +{{- define "setElement element value elementType"}} +{{- if eq .elementType "fr.Element"}} setElement(&{{.element}}, {{.value}}) +{{- else if eq .elementType "small_rational.SmallRational"}} {{.element}}.SetInterface({{.value}}) +{{- else}} + {{print "\"UNEXPECTED TYPE" .elementType "\""}} +{{- end}} +{{- end}} + +func sliceToElementSlice[T any](slice []T) ([]{{.ElementType}}, error) { + elementSlice := make([]{{.ElementType}}, len(slice)) + for i, v := range slice { + if _, err := {{setElement "elementSlice[i]" "v" .ElementType}}; err != nil { + return nil, err + } + } + return elementSlice, nil +} + +func sliceEquals(a []{{.ElementType}}, b []{{.ElementType}}) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if !a[i].Equal(&b[i]) { + return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) + } + } + return nil +} + +func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if err := sliceEquals(a[i],b[i]); err != nil { + return fmt.Errorf("at index %d: %w", i, err) + } + } + return nil +} \ No newline at end of file diff --git a/internal/generator/backend/template/imports.go.tmpl b/internal/generator/backend/template/imports.go.tmpl index c1cac6c9..9b3aa240 100644 --- a/internal/generator/backend/template/imports.go.tmpl +++ b/internal/generator/backend/template/imports.go.tmpl @@ -1,14 +1,18 @@ {{- define "import_fr" }} - {{- if eq .Curve "tinyfield"}} - fr "github.com/consensys/gnark/internal/tinyfield" + {{- if ne .AutoGenerateField "" }} + fr "github.com/consensys/gnark/internal/smallfields/{{ toLower .Curve }}" + {{- else if .OnlyField }} + fr "github.com/consensys/gnark-crypto/field/{{toLower .Curve}}" {{- else}} "github.com/consensys/gnark-crypto/ecc/{{toLower .Curve}}/fr" {{- end}} {{- end }} {{- define "import_fp" }} - {{- if eq .Curve "tinyfield"}} - fr "github.com/consensys/gnark/internal/tinyfield" + {{- if ne .AutoGenerateField "" }} + fp "github.com/consensys/gnark/internal/smallfields/{{ toLower .Curve }}" + {{- else if .OnlyField }} + fp "github.com/consensys/gnark-crypto/field/{{toLower .Curve}}" {{- else}} "github.com/consensys/gnark-crypto/ecc/{{toLower .Curve}}/fp" {{- end}} @@ -20,19 +24,15 @@ {{- end}} {{- define "import_curve" }} - {{- if ne .Curve "tinyfield"}} - curve "github.com/consensys/gnark-crypto/ecc/{{toLower .Curve}}" - {{- else }} + {{- if ne .AutoGenerateField "" }} "github.com/consensys/gnark-crypto/ecc" + {{- else}} + curve "github.com/consensys/gnark-crypto/ecc/{{toLower .Curve}}" {{- end}} {{- end }} {{- define "import_backend_cs" }} - {{- if eq .Curve "tinyfield"}} - "github.com/consensys/gnark/constraint/tinyfield" - {{- else}} - cs "github.com/consensys/gnark/constraint/{{toLower .Curve}}" - {{- end}} + cs "github.com/consensys/gnark/constraint/{{toLower .Curve }}" {{- end }} {{- define "import_fft" }} @@ -40,8 +40,8 @@ {{- end }} {{- define "import_witness" }} - {{- if eq .Curve "tinyfield"}} - {{toLower .CurveID}}witness "github.com/consensys/gnark/internal/tinyfield/witness" + {{- if ne .AutoGenerateField "" | or .OnlyField}} + {{toLower .CurveID}}witness "github.com/consensys/gnark/internal/smallfields/{{ .Curve }}/witness" {{- else}} {{toLower .CurveID}}witness "github.com/consensys/gnark/internal/backend/{{toLower .Curve}}/witness" {{- end}} @@ -68,7 +68,7 @@ {{- end}} {{- define "import_gkr"}} - "github.com/consensys/gnark-crypto/ecc/{{ toLower .Curve }}/fr/gkr" + gkr "github.com/consensys/gnark/internal/gkr/{{ toLower .Curve }}" {{- end}} {{- define "import_hash_to_field" }} diff --git a/internal/generator/backend/template/representations/coeff.go.tmpl b/internal/generator/backend/template/representations/coeff.go.tmpl index 02e4eeab..75990b70 100644 --- a/internal/generator/backend/template/representations/coeff.go.tmpl +++ b/internal/generator/backend/template/representations/coeff.go.tmpl @@ -36,7 +36,11 @@ func (ct *CoeffTable) toBytes() []byte { buf = binary.LittleEndian.AppendUint64(buf, ctLen) for _, c := range ct.Coefficients { for _, w := range c { + {{- if eq .ElementType "U64" -}} buf = binary.LittleEndian.AppendUint64(buf, w) + {{- else if eq .ElementType "U32" -}} + buf = binary.LittleEndian.AppendUint32(buf, w) + {{- end -}} } } @@ -58,14 +62,18 @@ func (ct *CoeffTable) fromBytes(buf []byte) error { var c fr.Element k := int(i) * fr.Bytes for j := 0; j < fr.Limbs; j++ { + {{- if eq .ElementType "U64" -}} c[j] = binary.LittleEndian.Uint64(buf[k + j * 8 : k + (j+1)*8]) + {{- else if eq .ElementType "U32" -}} + c[j] = binary.LittleEndian.Uint32(buf[k+j*4 : k+(j+1)*4]) + {{- end -}} } ct.Coefficients[i] = c } return nil } -func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { +func (ct *CoeffTable) AddCoeff(coeff constraint.{{ .ElementType }}) uint32 { c := (*fr.Element)(coeff[:]) var cID uint32 if c.IsZero() { @@ -91,7 +99,7 @@ func (ct *CoeffTable) AddCoeff(coeff constraint.Element) uint32 { return cID } -func (ct *CoeffTable) MakeTerm(coeff constraint.Element, variableID int) constraint.Term { +func (ct *CoeffTable) MakeTerm(coeff constraint.{{ .ElementType }}, variableID int) constraint.Term { cID := ct.AddCoeff(coeff) return constraint.Term{VID: uint32(variableID), CID: cID} } @@ -104,7 +112,7 @@ func (ct *CoeffTable) CoeffToString(cID int) string { // implements constraint.Field type field struct{} -var _ constraint.Field = &field{} +var _ constraint.Field[constraint.{{ .ElementType }}] = &field{} var ( two fr.Element @@ -123,7 +131,7 @@ func init() { -func (engine *field) FromInterface(i interface{}) constraint.Element { +func (engine *field) FromInterface(i interface{}) constraint.{{ .ElementType }} { var e fr.Element if _, err := e.SetInterface(i); err != nil { // need to clean that --> some code path are dissimilar @@ -132,43 +140,43 @@ func (engine *field) FromInterface(i interface{}) constraint.Element { b := utils.FromInterface(i) e.SetBigInt(&b) } - var r constraint.Element + var r constraint.{{ .ElementType }} copy(r[:], e[:]) return r } -func (engine *field) ToBigInt(c constraint.Element) *big.Int { +func (engine *field) ToBigInt(c constraint.{{ .ElementType }}) *big.Int { e := (*fr.Element)(c[:]) r := new(big.Int) e.BigInt(r) return r } -func (engine *field) Mul(a, b constraint.Element) constraint.Element { +func (engine *field) Mul(a, b constraint.{{ .ElementType}}) constraint.{{ .ElementType }} { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Mul(_a, _b) return a } -func (engine *field) Add(a, b constraint.Element) constraint.Element { +func (engine *field) Add(a, b constraint.{{ .ElementType }}) constraint.{{ .ElementType }} { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Add(_a, _b) return a } -func (engine *field) Sub(a, b constraint.Element) constraint.Element { +func (engine *field) Sub(a, b constraint.{{ .ElementType }}) constraint.{{ .ElementType }} { _a := (*fr.Element)(a[:]) _b := (*fr.Element)(b[:]) _a.Sub(_a, _b) return a } -func (engine *field) Neg(a constraint.Element) constraint.Element { +func (engine *field) Neg(a constraint.{{ .ElementType }}) constraint.{{ .ElementType }} { e := (*fr.Element)(a[:]) e.Neg(e) return a } -func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { +func (engine *field) Inverse(a constraint.{{ .ElementType }}) (constraint.{{ .ElementType }}, bool) { if a.IsZero() { return a, false } @@ -188,24 +196,24 @@ func (engine *field) Inverse(a constraint.Element) (constraint.Element, bool) { return a, true } -func (engine *field) IsOne(a constraint.Element) bool { +func (engine *field) IsOne(a constraint.{{ .ElementType }}) bool { e := (*fr.Element)(a[:]) return e.IsOne() } -func (engine *field) One() constraint.Element { +func (engine *field) One() constraint.{{ .ElementType }} { e := fr.One() - var r constraint.Element + var r constraint.{{ .ElementType }} copy(r[:], e[:]) return r } -func (engine *field) String(a constraint.Element) string { +func (engine *field) String(a constraint.{{ .ElementType }}) string { e := (*fr.Element)(a[:]) return e.String() } -func (engine *field) Uint64(a constraint.Element) (uint64, bool) { +func (engine *field) Uint64(a constraint.{{ .ElementType }}) (uint64, bool) { e := (*fr.Element)(a[:]) if !e.IsUint64() { return 0, false diff --git a/internal/generator/backend/template/representations/gkr.go.tmpl b/internal/generator/backend/template/representations/gkr.go.tmpl deleted file mode 100644 index 9030cc43..00000000 --- a/internal/generator/backend/template/representations/gkr.go.tmpl +++ /dev/null @@ -1,226 +0,0 @@ -import ( - "fmt" - {{- template "import_fr" .}} - {{- template "import_gkr" .}} - {{- template "import_polynomial" .}} - fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" - "github.com/consensys/gnark-crypto/utils" - "github.com/consensys/gnark/constraint" - hint "github.com/consensys/gnark/constraint/solver" - algo_utils "github.com/consensys/gnark/internal/utils" - "hash" - "math/big" - "sync" -) - -type GkrSolvingData struct { - assignments gkr.WireAssignment - circuit gkr.Circuit - memoryPool polynomial.Pool - workers *utils.WorkerPool -} - -func convertCircuit(noPtr constraint.GkrCircuit) (gkr.Circuit, error) { - resCircuit := make(gkr.Circuit, len(noPtr)) - var found bool - for i := range noPtr { - if resCircuit[i].Gate, found = gkr.Gates[noPtr[i].Gate]; !found && noPtr[i].Gate != "" { - return nil, fmt.Errorf("gate \"%s\" not found", noPtr[i].Gate) - } - resCircuit[i].Inputs = algo_utils.Map(noPtr[i].Inputs, algo_utils.SlicePtrAt(resCircuit)) - } - return resCircuit, nil -} - -func (d *GkrSolvingData) init(info constraint.GkrInfo) (assignment gkrAssignment, err error) { - if d.circuit, err = convertCircuit(info.Circuit); err != nil { - return - } - d.memoryPool = polynomial.NewPool(d.circuit.MemoryRequirements(info.NbInstances)...) - d.workers = utils.NewWorkerPool() - - assignment = make(gkrAssignment, len(d.circuit)) - d.assignments = make(gkr.WireAssignment, len(d.circuit)) - for i := range assignment { - assignment[i] = d.memoryPool.Make(info.NbInstances) - d.assignments[&d.circuit[i]] = assignment[i] - } - return -} - -func (d *GkrSolvingData) dumpAssignments() { - for _, p := range d.assignments { - d.memoryPool.Dump(p) - } -} - -// this module assumes that wire and instance indexes respect dependencies - -type gkrAssignment [][]fr.Element //gkrAssignment is indexed wire first, instance second - -func (a gkrAssignment) setOuts(circuit constraint.GkrCircuit, outs []*big.Int) { - outsI := 0 - for i := range circuit { - if circuit[i].IsOutput() { - for j := range a[i] { - a[i][j].BigInt(outs[outsI]) - outsI++ - } - } - } - // Check if outsI == len(outs)? -} - -func GkrSolveHint(info constraint.GkrInfo, solvingData *GkrSolvingData) hint.Hint { - return func(_ *big.Int, ins, outs []*big.Int) error { - // assumes assignmentVector is arranged wire first, instance second in order of solution - circuit := info.Circuit - nbInstances := info.NbInstances - offsets := info.AssignmentOffsets() - assignment, err := solvingData.init(info) - if err != nil { - return err - } - chunks := circuit.Chunks(nbInstances) - - solveTask := func(chunkOffset int) utils.Task { - return func(startInChunk, endInChunk int) { - start := startInChunk + chunkOffset - end := endInChunk + chunkOffset - inputs := solvingData.memoryPool.Make(info.MaxNIns) - dependencyHeads := make([]int, len(circuit)) - for wI, w := range circuit { - dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { - return w.Dependencies[i].InputInstance - }, len(w.Dependencies), start) - } - - for instanceI := start; instanceI < end; instanceI++ { - for wireI, wire := range circuit { - if wire.IsInput() { - if dependencyHeads[wireI] < len(wire.Dependencies) && instanceI == wire.Dependencies[dependencyHeads[wireI]].InputInstance { - dep := wire.Dependencies[dependencyHeads[wireI]] - assignment[wireI][instanceI].Set(&assignment[dep.OutputWire][dep.OutputInstance]) - dependencyHeads[wireI]++ - } else { - assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) - } - } else { - // assemble the inputs - inputIndexes := info.Circuit[wireI].Inputs - for i, inputI := range inputIndexes { - inputs[i].Set(&assignment[inputI][instanceI]) - } - gate := solvingData.circuit[wireI].Gate - assignment[wireI][instanceI] = gate.Evaluate(inputs[:len(inputIndexes)]...) - } - } - } - solvingData.memoryPool.Dump(inputs) - } - } - - start := 0 - for _, end := range chunks { - solvingData.workers.Submit(end-start, solveTask(start), 1024).Wait() - start = end - } - - assignment.setOuts(info.Circuit, outs) - - return nil - } -} - -func frToBigInts(dst []*big.Int, src []fr.Element) { - for i := range src { - src[i].BigInt(dst[i]) - } -} - -func GkrProveHint(hashName string, data *GkrSolvingData) hint.Hint { - - return func(_ *big.Int, ins, outs []*big.Int) error { - insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called - b := make([]byte, fr.Bytes) - i.FillBytes(b) - return b[:] - }) - - hsh, err := GetHashBuilder(hashName) - if err != nil { - return err - } - - proof, err := gkr.Prove(data.circuit, data.assignments, fiatshamir.WithHash(hsh(), insBytes...), gkr.WithPool(&data.memoryPool), gkr.WithWorkers(data.workers)) - if err != nil { - return err - } - - // serialize proof: TODO: In gnark-crypto? - offset := 0 - for i := range proof { - for _, poly := range proof[i].PartialSumPolys { - frToBigInts(outs[offset:], poly) - offset += len(poly) - } - if proof[i].FinalEvalProof != nil { - finalEvalProof := proof[i].FinalEvalProof.([]fr.Element) - frToBigInts(outs[offset:], finalEvalProof) - offset += len(finalEvalProof) - } - } - - data.dumpAssignments() - - return nil - - } -} - -// TODO: Move to gnark-crypto -var ( - hashBuilderRegistry = make(map[string]func() hash.Hash) - hasBuilderLock sync.RWMutex -) - -func RegisterHashBuilder(name string, builder func() hash.Hash) { - hasBuilderLock.Lock() - defer hasBuilderLock.Unlock() - hashBuilderRegistry[name] = builder -} - -func GetHashBuilder(name string) (func() hash.Hash, error) { - hasBuilderLock.RLock() - defer hasBuilderLock.RUnlock() - builder, ok := hashBuilderRegistry[name] - if !ok { - return nil, fmt.Errorf("hash function not found") - } - return builder, nil -} - - -// For testing purposes -type ConstPseudoHash int - -func (c ConstPseudoHash) Write(p []byte) (int, error) { - return len(p), nil -} - -func (c ConstPseudoHash) Sum([]byte) []byte { - var x fr.Element - x.SetInt64(int64(c)) - res := x.Bytes() - return res[:] -} - -func (c ConstPseudoHash) Reset() {} - -func (c ConstPseudoHash) Size() int { - return fr.Bytes -} - -func (c ConstPseudoHash) BlockSize() int { - return fr.Bytes -} diff --git a/internal/generator/backend/template/representations/solver.go.tmpl b/internal/generator/backend/template/representations/solver.go.tmpl index 96ef4d3a..fd685e6e 100644 --- a/internal/generator/backend/template/representations/solver.go.tmpl +++ b/internal/generator/backend/template/representations/solver.go.tmpl @@ -12,6 +12,9 @@ import ( "github.com/rs/zerolog" "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/field/pool" + "github.com/consensys/gnark/constraint/solver/gkrgates" + gkr "github.com/consensys/gnark/internal/gkr/{{ toLower .Curve }}" + "github.com/consensys/gnark/internal/gkr/gkrtypes" {{ template "import_fr" . }} ) @@ -40,10 +43,14 @@ func newSolver(cs *system, witness fr.Vector, opts ...csolver.Option) (*solver, {{ if not .NoGKR -}} // add GKR options to overwrite the placeholder if cs.GkrInfo.Is() { - var gkrData GkrSolvingData + var gkrData gkr.SolvingData + solvingInfo, err := gkrtypes.StoringToSolvingInfo(cs.GkrInfo, gkrgates.Get) + if err != nil { + return nil, err + } opts = append(opts, - csolver.OverrideHint(cs.GkrInfo.SolveHintID, GkrSolveHint(cs.GkrInfo, &gkrData)), - csolver.OverrideHint(cs.GkrInfo.ProveHintID, GkrProveHint(cs.GkrInfo.HashName, &gkrData))) + csolver.OverrideHint(cs.GkrInfo.SolveHintID, gkr.SolveHint(solvingInfo, &gkrData)), + csolver.OverrideHint(cs.GkrInfo.ProveHintID, gkr.ProveHint(cs.GkrInfo.HashName, &gkrData))) } {{ end -}} @@ -337,18 +344,18 @@ func (solver *solver) divByCoeff(res *fr.Element, cID uint32) { // Implement constraint.Solver -func (s *solver) GetValue(cID, vID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetValue(cID, vID uint32) constraint.{{ .ElementType }} { + var r constraint.{{ .ElementType }} e := s.computeTerm(constraint.Term{CID:cID,VID: vID}) copy(r[:], e[:]) return r } -func (s *solver) GetCoeff(cID uint32) constraint.Element { - var r constraint.Element +func (s *solver) GetCoeff(cID uint32) constraint.{{ .ElementType }} { + var r constraint.{{ .ElementType }} copy(r[:], s.Coefficients[cID][:]) return r } -func (s *solver) SetValue(vID uint32, f constraint.Element) { +func (s *solver) SetValue(vID uint32, f constraint.{{ .ElementType }}) { s.set(int(vID), *(*fr.Element)(f[:])) } @@ -358,7 +365,7 @@ func (s *solver) IsSolved(vID uint32) bool { // Read interprets input calldata as either a LinearExpression (if R1CS) or a Term (if Plonkish), // evaluates it and return the result and the number of uint32 word read. -func (s *solver) Read(calldata []uint32) (constraint.Element, int) { +func (s *solver) Read(calldata []uint32) (constraint.{{ .ElementType }}, int) { if s.Type == constraint.SystemSparseR1CS { if calldata[0] != 1 { panic("invalid calldata") @@ -374,7 +381,7 @@ func (s *solver) Read(calldata []uint32) (constraint.Element, int) { j+=2 } - var ret constraint.Element + var ret constraint.{{ .ElementType }} copy(ret[:], r[:]) return ret, j } @@ -400,7 +407,7 @@ func (solver *solver) processInstruction(pi constraint.PackedInstruction, scratc } // blueprint declared "I know how to solve this." - if bc, ok := blueprint.(constraint.BlueprintSolvable); ok { + if bc, ok := blueprint.(constraint.BlueprintSolvable[constraint.{{ .ElementType }}]); ok { if err := bc.Solve(solver, inst); err != nil { return solver.wrapErrWithDebugInfo(cID, err) } diff --git a/internal/generator/backend/template/representations/system.go.tmpl b/internal/generator/backend/template/representations/system.go.tmpl index 1c627602..07dc757f 100644 --- a/internal/generator/backend/template/representations/system.go.tmpl +++ b/internal/generator/backend/template/representations/system.go.tmpl @@ -6,6 +6,7 @@ import ( "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/logger" "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/internal/gkr/gkrinfo" "github.com/consensys/gnark-crypto/ecc" @@ -61,7 +62,7 @@ func (cs *system) Solve(witness witness.Witness, opts ...csolver.Option) (any, e // reset the stateful blueprints for i := range cs.Blueprints { - if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful); ok { + if b, ok := cs.Blueprints[i].(constraint.BlueprintStateful[constraint.{{ .ElementType }}]); ok { b.Reset() } } @@ -131,7 +132,7 @@ func (cs *system) CurveID() ecc.ID { return ecc.{{.CurveID}} } -func (cs *system) GetCoefficient(i int) (r constraint.Element) { +func (cs *system) GetCoefficient(i int) (r constraint.{{ .ElementType }}) { copy(r[:], cs.Coefficients[i][:]) return } @@ -294,6 +295,6 @@ func (t *SparseR1CSSolution) ReadFrom(r io.Reader) (int64, error) { } -func (s *system) AddGkr(gkr constraint.GkrInfo) error { +func (s *system) AddGkr(gkr gkrinfo.StoringInfo) error { return s.System.AddGkr(gkr) } \ No newline at end of file diff --git a/internal/generator/backend/template/representations/tests/r1cs.go.tmpl b/internal/generator/backend/template/representations/tests/r1cs.go.tmpl index af4480ee..ca4c866a 100644 --- a/internal/generator/backend/template/representations/tests/r1cs.go.tmpl +++ b/internal/generator/backend/template/representations/tests/r1cs.go.tmpl @@ -3,10 +3,14 @@ import ( "bytes" "testing" "reflect" + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/internal/backend/circuits" + {{- if eq .ElementType "U32"}} + "github.com/consensys/gnark/internal/widecommitter" + {{- end }} "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -31,8 +35,15 @@ func TestSerialization(t *testing.T) { return } {{- end}} + builder := r1cs.NewBuilder[constraint.{{ .ElementType }}] + {{- if eq .ElementType "U32"}} + if name == "commit" { + // smallfield builders do not support commitment. We use the wrapper which has the methods + builder = widecommitter.From(builder) + } + {{- end }} - r1cs1, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs1, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -41,7 +52,7 @@ func TestSerialization(t *testing.T) { } // compile a second time to ensure determinism - r1cs2, err := frontend.Compile(fr.Modulus(), r1cs.NewBuilder, tc.Circuit) + r1cs2, err := frontend.CompileGeneric(fr.Modulus(), builder, tc.Circuit) if err != nil { t.Fatal(err) } @@ -149,8 +160,8 @@ func BenchmarkSolve(b *testing.B) { } b.Run("scs", func(b *testing.B) { - var c circuit - ccs, err := frontend.Compile(fr.Modulus(),scs.NewBuilder, &c) + var c circuit + ccs, err := frontend.CompileGeneric[constraint.{{ .ElementType }}](fr.Modulus(), scs.NewBuilder, &c) if err != nil { b.Fatal(err) } @@ -163,8 +174,8 @@ func BenchmarkSolve(b *testing.B) { }) b.Run("r1cs", func(b *testing.B) { - var c circuit - ccs, err := frontend.Compile(fr.Modulus(),r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) + var c circuit + ccs, err := frontend.CompileGeneric[constraint.{{ .ElementType }}](fr.Modulus(), r1cs.NewBuilder, &c, frontend.WithCompressThreshold(10)) if err != nil { b.Fatal(err) } diff --git a/internal/generator/backend/template/zkpschemes/groth16/mpcsetup/phase1.go.tmpl b/internal/generator/backend/template/zkpschemes/groth16/mpcsetup/phase1.go.tmpl index 8976eb4d..96f872f9 100644 --- a/internal/generator/backend/template/zkpschemes/groth16/mpcsetup/phase1.go.tmpl +++ b/internal/generator/backend/template/zkpschemes/groth16/mpcsetup/phase1.go.tmpl @@ -207,10 +207,10 @@ func (p *Phase1) Verify(next *Phase1) error { } return mpcsetup.SameRatioMany( - p.parameters.G1.Tau, - p.parameters.G2.Tau, - p.parameters.G1.AlphaTau, - p.parameters.G1.BetaTau, + next.parameters.G1.Tau, + next.parameters.G2.Tau, + next.parameters.G1.AlphaTau, + next.parameters.G1.BetaTau, ) } diff --git a/internal/gkr/bls12-377/gate_testing.go b/internal/gkr/bls12-377/gate_testing.go new file mode 100644 index 00000000..415a5ff5 --- /dev/null +++ b/internal/gkr/bls12-377/gate_testing.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + + "slices" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// IsGateFunctionAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func IsGateFunctionAdditive(f gkr.GateFunction, i, nbIn int) bool { + fWrapped := api.convertFunc(f) + + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 := fWrapped(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := fWrapped(in...) + + y2.Sub(y2, y1) + y1.Sub(y1, y0) + + if !y2.Equal(y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 = fWrapped(in...) + + y1.Sub(y1, y0) + + return y1.Equal(y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f gateFunctionFr) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i].Set(f(fIn...)) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +// FindGateFunctionDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func FindGateFunctionDegree(f gkr.GateFunction, max, nbIn int) (int, error) { + fFr := api.convertFunc(f) + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := fFr.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, gkrtypes.ErrZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func VerifyGateFunctionDegree(f gkr.GateFunction, claimedDegree, nbIn int) error { + fFr := api.convertFunc(f) + if p := fFr.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return gkrtypes.ErrZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} diff --git a/internal/gkr/bls12-377/gkr.go b/internal/gkr/bls12-377/gkr.go new file mode 100644 index 00000000..f5dfad02 --- /dev/null +++ b/internal/gkr/bls12-377/gkr.go @@ -0,0 +1,816 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "sync" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// The goal is to prove/verify evaluations of many instances of the same circuit + +// WireAssignment is assignment of values to the same wire across many instances of the circuit +type WireAssignment []polynomial.MultiLin + +type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) + +// eqTimesGateEvalSumcheckLazyClaims is a lazy claim for sumcheck (verifier side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the checking of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckLazyClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ), allegedly + manager *claimsManager // WARNING: Circular references +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) getWire() *gkrtypes.Wire { + return e.manager.wires[e.wireI] +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) claimsNum() int { + return len(e.evaluationPoints) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) varsNum() int { + return len(e.evaluationPoints[0]) +} + +// combinedSum returns ∑ᵢ aⁱ yᵢ +func (e *eqTimesGateEvalSumcheckLazyClaims) combinedSum(a fr.Element) fr.Element { + evalsAsPoly := polynomial.Polynomial(e.claimedEvaluations) + return evalsAsPoly.Eval(&a) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) degree(int) int { + return 1 + e.manager.wires[e.wireI].Gate.Degree() +} + +// verifyFinalEval finalizes the verification of w. +// The prover's claims w(xᵢ) = yᵢ have already been reduced to verifying +// ∑ cⁱ eq(xᵢ, r) w(r) = purportedValue. ( c is combinationCoeff ) +// Both purportedValue and the vector r have been randomized during the sumcheck protocol. +// By taking the w term out of the sum we get the equivalent claim that +// for E := ∑ eq(xᵢ, r), it must be that E w(r) = purportedValue. +// If w is an input wire, the verifier can directly check its evaluation at r. +// Otherwise, the prover makes claims about the evaluation of w's input wires, +// wᵢ, at r, to be verified later. +// The claims are communicated through the proof parameter. +// The verifier checks here if the claimed evaluations of wᵢ(r) are consistent with +// the main claim, by checking E w(wᵢ(r)...) = purportedValue. +func (e *eqTimesGateEvalSumcheckLazyClaims) verifyFinalEval(r []fr.Element, combinationCoeff, purportedValue fr.Element, uniqueInputEvaluations []fr.Element) error { + // the eq terms ( E ) + numClaims := len(e.evaluationPoints) + evaluation := polynomial.EvalEq(e.evaluationPoints[numClaims-1], r) + for i := numClaims - 2; i >= 0; i-- { + evaluation.Mul(&evaluation, &combinationCoeff) + eq := polynomial.EvalEq(e.evaluationPoints[i], r) + evaluation.Add(&evaluation, &eq) + } + + wire := e.manager.wires[e.wireI] + + // the w(...) term + var gateEvaluation fr.Element + if wire.IsInput() { // just compute w(r) + gateEvaluation = e.manager.assignment[e.wireI].Evaluate(r, e.manager.memPool) + } else { // proof contains the evaluations of the inputs, but avoids repetition in case multiple inputs come from the same wire + injection, injectionLeftInv := + e.manager.wires.ClaimPropagationInfo(e.wireI) + + if len(injection) != len(uniqueInputEvaluations) { + return fmt.Errorf("%d input wire evaluations given, %d expected", len(uniqueInputEvaluations), len(injection)) + } + + for uniqueI, i := range injection { // map from unique to all + e.manager.add(wire.Inputs[i], r, uniqueInputEvaluations[uniqueI]) + } + + inputEvaluations := make([]frontend.Variable, len(wire.Inputs)) + for i, uniqueI := range injectionLeftInv { // map from all to unique + inputEvaluations[i] = &uniqueInputEvaluations[uniqueI] + } + + gateEvaluation.Set(wire.Gate.Evaluate(api, inputEvaluations...).(*fr.Element)) + } + + evaluation.Mul(&evaluation, &gateEvaluation) + + if evaluation.Equal(&purportedValue) { + return nil + } + return errors.New("incompatible evaluations") +} + +// eqTimesGateEvalSumcheckClaims is a claim for sumcheck (prover side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the proving of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ) + manager *claimsManager + + input []polynomial.MultiLin // input[i](h₁, ..., hₘ₋ⱼ) = wᵢ(r₁, r₂, ..., rⱼ₋₁, h₁, ..., hₘ₋ⱼ) + + eq polynomial.MultiLin // E := ∑ᵢ cⁱ eq(xᵢ, -) +} + +func (c *eqTimesGateEvalSumcheckClaims) getWire() *gkrtypes.Wire { + return c.manager.wires[c.wireI] +} + +// combine the multiple claims into one claim using a random combination (combinationCoeff or c). +// From the original multiple claims of w(xᵢ) = yᵢ, we get a single claim +// ∑ᵢ,ₕ cⁱ eq(xᵢ, h) w(h) = ∑ᵢ cⁱ yᵢ, where h iterates over the hypercube (circuit instances) and +// i iterates over the claims. +// Equivalently, we could say ∑ᵢ cⁱ yᵢ = ∑ₕ,ᵢ cⁱ eq(xᵢ, h) w(h) = ∑ₕ w(h) ∑ᵢ cⁱ eq(xᵢ, h). +// Thus if we initially compute E := ∑ᵢ cⁱ eq(xᵢ, -), our claim will find the simpler form +// ∑ᵢ cⁱ yᵢ = ∑ₕ w(h) E(h), where the sum-checked polynomial is of degree deg(g) + 1, +// and deg(g) is the total degree of the polynomial defining the gate g of which w is the output. +// The output of combine is the first sumcheck claim, i.e. ∑₍ₕ₁,ₕ₂,...₎ w(X, h₁, h₂, ...) E(X, h₁, h₂, ...).. +func (c *eqTimesGateEvalSumcheckClaims) combine(combinationCoeff fr.Element) polynomial.Polynomial { + varsNum := c.varsNum() + eqLength := 1 << varsNum + claimsNum := c.claimsNum() + // initialize the eq tables ( E ) + c.eq = c.manager.memPool.Make(eqLength) + + c.eq[0].SetOne() + c.eq.Eq(c.evaluationPoints[0]) + + // E := eq(x₀, -) + newEq := polynomial.MultiLin(c.manager.memPool.Make(eqLength)) + aI := combinationCoeff + + // E += cⁱ eq(xᵢ, -) + for k := 1; k < claimsNum; k++ { + newEq[0].Set(&aI) + + c.eqAcc(c.eq, newEq, c.evaluationPoints[k]) + + if k+1 < claimsNum { + aI.Mul(&aI, &combinationCoeff) + } + } + + c.manager.memPool.Dump(newEq) + + return c.computeGJ() +} + +// eqAcc sets m to an eq table at q and then adds it to e. +// m <- eq(q, -). +// e <- e + m +func (c *eqTimesGateEvalSumcheckClaims) eqAcc(e, m polynomial.MultiLin, q []fr.Element) { + n := len(q) + + //At the end of each iteration, m(h₁, ..., hₙ) = eq(q₁, ..., qᵢ₊₁, h₁, ..., hᵢ₊₁) + for i := range q { // In the comments we use a 1-based index so q[i] = qᵢ₊₁ + // go through all assignments of (b₁, ..., bᵢ) ∈ {0,1}ⁱ + const threshold = 1 << 6 + k := 1 << i + if k < threshold { + for j := 0; j < k; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + } else { + c.manager.workers.Submit(k, func(start, end int) { + for j := start; j < end; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + }, 1024).Wait() + } + + } + c.manager.workers.Submit(len(e), func(start, end int) { + for i := start; i < end; i++ { + e[i].Add(&e[i], &m[i]) + } + }, 512).Wait() +} + +// computeGJ: gⱼ = ∑_{0≤h<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, h...) = ∑_{0≤i<2ⁿ⁻ʲ} E(r₁, ..., Xⱼ, h...) g( w₀(r₁, ..., Xⱼ, h...), ... ). +// the polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). +// The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). By convention, g₀ is a constant polynomial equal to the claimed sum. +func (c *eqTimesGateEvalSumcheckClaims) computeGJ() polynomial.Polynomial { + + wire := c.getWire() + degGJ := 1 + wire.Gate.Degree() // guaranteed to be no smaller than the actual deg(gⱼ) + nbGateIn := len(c.input) + + // Both E and wᵢ (the input wires and the eq table) are multilinear, thus + // they are linear in Xⱼ. + // So for f ∈ { E(r₁, ..., Xⱼ, h...) } ∪ {wᵢ(r₁, ..., Xⱼ, h...) }, so f(m) = m×(f(1) - f(0)) + f(0), and f(0), f(1) are easily computed from the bookkeeping tables. + // ml are such multilinear polynomials the evaluations of which over different values of Xⱼ are computed in this stepwise manner. + ml := make([]polynomial.MultiLin, nbGateIn+1) // shortcut to the evaluations of the multilinear polynomials over the hypercube + ml[0] = c.eq + copy(ml[1:], c.input) + + sumSize := len(c.eq) / 2 // the range of h, over which we sum + + // Perf-TODO: Collate once at claim "combination" time and not again. then, even folding can be done in one operation every time "next" is called + + gJ := make([]fr.Element, degGJ) + var mu sync.Mutex + computeAll := func(start, end int) { // compute method to allow parallelization across instances + var step fr.Element + + res := make([]fr.Element, degGJ) + + // evaluations of ml, laid out as: + // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), + // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), + // ... + // ml[0](degGJ, h...), ml[2](degGJ, h...), ..., ml[len(ml)-1](degGJ, h...) + mlEvals := make([]fr.Element, degGJ*len(ml)) + gateInput := make([]frontend.Variable, nbGateIn) + + for h := start; h < end; h++ { // h counts across instances + + evalAt1Index := sumSize + h + for k := range ml { + // d = 0 + mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1. Can be taken directly from the table. + step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for d := 1; d < degGJ; d++ { + mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + } + } + + eIndex := 0 // index for where the current eq term is + nextEIndex := len(ml) + for d := range degGJ { + for i := range gateInput { + gateInput[i] = &mlEvals[eIndex+1+i] + } + summand := wire.Gate.Evaluate(api, gateInput...).(*fr.Element) + summand.Mul(summand, &mlEvals[eIndex]) + res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + } + } + mu.Lock() + for i := range gJ { + gJ[i].Add(&gJ[i], &res[i]) // collect into the complete sum + } + mu.Unlock() + } + + const minBlockSize = 64 + + if sumSize < minBlockSize { + // no parallelization + computeAll(0, sumSize) + } else { + c.manager.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } + + return gJ +} + +// next first folds the input and E polynomials at the given verifier challenge then computes the new gⱼ. +// Thus, j <- j+1 and rⱼ = challenge. +func (c *eqTimesGateEvalSumcheckClaims) next(challenge fr.Element) polynomial.Polynomial { + const minBlockSize = 512 + n := len(c.eq) / 2 + if n < minBlockSize { + // no parallelization + for i := 0; i < len(c.input); i++ { + c.input[i].Fold(challenge) + } + c.eq.Fold(challenge) + } else { + wgs := make([]*sync.WaitGroup, len(c.input)) + for i := 0; i < len(c.input); i++ { + wgs[i] = c.manager.workers.Submit(n, c.input[i].FoldParallel(challenge), minBlockSize) + } + c.manager.workers.Submit(n, c.eq.FoldParallel(challenge), minBlockSize).Wait() + for _, wg := range wgs { + wg.Wait() + } + } + + return c.computeGJ() +} + +func (c *eqTimesGateEvalSumcheckClaims) varsNum() int { + return len(c.evaluationPoints[0]) +} + +func (c *eqTimesGateEvalSumcheckClaims) claimsNum() int { + return len(c.claimedEvaluations) +} + +// proveFinalEval provides the values wᵢ(r₁, ..., rₙ) +func (c *eqTimesGateEvalSumcheckClaims) proveFinalEval(r []fr.Element) []fr.Element { + //defer the proof, return list of claims + + injection, _ := c.manager.wires.ClaimPropagationInfo(c.wireI) // TODO @Tabaie: Instead of doing this last, we could just have fewer input in the first place; not that likely to happen with single gates, but more so with layers. + evaluations := make([]fr.Element, len(injection)) + for i, gateInputI := range injection { + wI := c.input[gateInputI] + wI.Fold(r[len(r)-1]) // We already have wᵢ(r₁, ..., rₙ₋₁, hₙ) in a table. Only one more fold required. + c.manager.add(c.getWire().Inputs[gateInputI], r, wI[0]) + evaluations[i] = wI[0] + } + + c.manager.memPool.Dump(c.claimedEvaluations, c.eq) + + return evaluations +} + +type claimsManager struct { + claims []*eqTimesGateEvalSumcheckLazyClaims + assignment WireAssignment + memPool *polynomial.Pool + workers *utils.WorkerPool + wires gkrtypes.Wires +} + +func newClaimsManager(wires []*gkrtypes.Wire, assignment WireAssignment, o settings) (manager claimsManager) { + manager.assignment = assignment + manager.claims = make([]*eqTimesGateEvalSumcheckLazyClaims, len(wires)) + manager.memPool = o.pool + manager.workers = o.workers + manager.wires = wires + + for i, wire := range wires { + + manager.claims[i] = &eqTimesGateEvalSumcheckLazyClaims{ + wireI: i, + evaluationPoints: make([][]fr.Element, 0, wire.NbClaims()), + claimedEvaluations: manager.memPool.Make(wire.NbClaims()), + manager: &manager, + } + } + return +} + +func (m *claimsManager) add(wire int, evaluationPoint []fr.Element, evaluation fr.Element) { + claim := m.claims[wire] + i := len(claim.evaluationPoints) + claim.claimedEvaluations[i] = evaluation + claim.evaluationPoints = append(claim.evaluationPoints, evaluationPoint) +} + +func (m *claimsManager) getLazyClaim(wire int) *eqTimesGateEvalSumcheckLazyClaims { + return m.claims[wire] +} + +func (m *claimsManager) getClaim(wireI int) *eqTimesGateEvalSumcheckClaims { + lazy := m.claims[wireI] + wire := m.wires[wireI] + res := &eqTimesGateEvalSumcheckClaims{ + wireI: wireI, + evaluationPoints: lazy.evaluationPoints, + claimedEvaluations: lazy.claimedEvaluations, + manager: m, + } + + if wire.IsInput() { + res.input = []polynomial.MultiLin{m.memPool.Clone(m.assignment[wireI])} + } else { + res.input = make([]polynomial.MultiLin, len(wire.Inputs)) + + for inputI, inputW := range wire.Inputs { + res.input[inputI] = m.memPool.Clone(m.assignment[inputW]) //will be edited later, so must be deep copied + } + } + return res +} + +func (m *claimsManager) deleteClaim(wire int) { + m.claims[wire].manager = nil + m.claims[wire] = nil +} + +type settings struct { + pool *polynomial.Pool + sorted []*gkrtypes.Wire + transcript *fiatshamir.Transcript + transcriptPrefix string + nbVars int + workers *utils.WorkerPool +} + +type Option func(*settings) + +func WithPool(pool *polynomial.Pool) Option { + return func(options *settings) { + options.pool = pool + } +} + +func WithSortedCircuit(sorted []*gkrtypes.Wire) Option { + return func(options *settings) { + options.sorted = sorted + } +} + +func WithWorkers(workers *utils.WorkerPool) Option { + return func(options *settings) { + options.workers = workers + } +} + +func setup(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (settings, error) { + var o settings + var err error + for _, option := range options { + option(&o) + } + + o.nbVars = assignment.NumVars() + nbInstances := assignment.NumInstances() + if 1< 1 { //combine the claims + size++ + } + size += logNbInstances // full run of sumcheck on logNbInstances variables + } + + nums := make([]string, max(len(sorted), logNbInstances)) + for i := range nums { + nums[i] = strconv.Itoa(i) + } + + challenges := make([]string, size) + + // output wire claims + firstChallengePrefix := prefix + "fC." + for j := 0; j < logNbInstances; j++ { + challenges[j] = firstChallengePrefix + nums[j] + } + j := logNbInstances + for i := len(sorted) - 1; i >= 0; i-- { + if sorted[i].NoProof() { + continue + } + wirePrefix := prefix + "w" + nums[i] + "." + + if sorted[i].NbClaims() > 1 { + challenges[j] = wirePrefix + "comb" + j++ + } + + partialSumPrefix := wirePrefix + "pSP." + for k := 0; k < logNbInstances; k++ { + challenges[j] = partialSumPrefix + nums[k] + j++ + } + } + return challenges +} + +func getFirstChallengeNames(logNbInstances int, prefix string) []string { + res := make([]string, logNbInstances) + firstChallengePrefix := prefix + "fC." + for i := 0; i < logNbInstances; i++ { + res[i] = firstChallengePrefix + strconv.Itoa(i) + } + return res +} + +func getChallenges(transcript *fiatshamir.Transcript, names []string) ([]fr.Element, error) { + res := make([]fr.Element, len(names)) + for i, name := range names { + if bytes, err := transcript.ComputeChallenge(name); err == nil { + res[i].SetBytes(bytes) + } else { + return nil, err + } + } + return res, nil +} + +// Prove consistency of the claimed assignment +func Prove(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (Proof, error) { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return nil, err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + proof := make(Proof, len(c)) + // firstChallenge called rho in the paper + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return nil, err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + claim := claims.getClaim(i) + if wire.NoProof() { // input wires with one claim only + proof[i] = sumcheckProof{ + partialSumPolys: []polynomial.Polynomial{}, + finalEvalProof: []fr.Element{}, + } + } else { + if proof[i], err = sumcheckProve( + claim, fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err != nil { + return proof, err + } + + baseChallenge = make([][]byte, len(proof[i].finalEvalProof)) + for j := range proof[i].finalEvalProof { + baseChallenge[j] = proof[i].finalEvalProof[j].Marshal() + } + } + // the verifier checks a single claim about input wires itself + claims.deleteClaim(i) + } + + return proof, nil +} + +// Verify the consistency of the claimed output with the claimed input +// Unlike in Prove, the assignment argument need not be complete +func Verify(c gkrtypes.Circuit, assignment WireAssignment, proof Proof, transcriptSettings fiatshamir.Settings, options ...Option) error { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + proofW := proof[i] + claim := claims.getLazyClaim(i) + if wire.NoProof() { // input wires with one claim only + // make sure the proof is empty + if len(proofW.finalEvalProof) != 0 || len(proofW.partialSumPolys) != 0 { + return errors.New("no proof allowed for input wire with a single claim") + } + + if wire.NbClaims() == 1 { // input wire + // simply evaluate and see if it matches + evaluation := assignment[i].Evaluate(claim.evaluationPoints[0], claims.memPool) + if !claim.claimedEvaluations[0].Equal(&evaluation) { + return errors.New("incorrect input wire claim") + } + } + } else if err = sumcheckVerify( + claim, proof[i], fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err == nil { // incorporate prover claims about w's input into the transcript + baseChallenge = make([][]byte, len(proofW.finalEvalProof)) + for j := range baseChallenge { + baseChallenge[j] = proofW.finalEvalProof[j].Marshal() + } + } else { + return fmt.Errorf("sumcheck proof rejected: %v", err) //TODO: Any polynomials to dump? + } + claims.deleteClaim(i) + } + return nil +} + +// Complete the circuit evaluation from input values +func (a WireAssignment) Complete(wires gkrtypes.Wires) WireAssignment { + + nbInstances := a.NumInstances() + maxNbIns := 0 + + for i, w := range wires { + maxNbIns = max(maxNbIns, len(w.Inputs)) + if len(a[i]) != nbInstances { + a[i] = make([]fr.Element, nbInstances) + } + } + + ins := make([]fr.Element, maxNbIns) + for i := range nbInstances { + for wI, w := range wires { + if !w.IsInput() { + for inI, in := range w.Inputs { + ins[inI] = a[in][i] + } + a[wI][i].Set(api.evaluate(w.Gate.Evaluate, ins[:len(w.Inputs)]...)) + } + } + } + + return a +} + +func (a WireAssignment) NumInstances() int { + for _, aW := range a { + return len(aW) + } + panic("empty assignment") +} + +func (a WireAssignment) NumVars() int { + for _, aW := range a { + return aW.NumVars() + } + panic("empty assignment") +} + +// SerializeToBigInts flattens a proof object into the given slice of big.Ints +// useful in gnark hints. +func (p Proof) SerializeToBigInts(outs []*big.Int) error { + offset := 0 + for i := range p { + for _, poly := range p[i].partialSumPolys { + frToBigInts(outs[offset:], poly) + offset += len(poly) + } + if p[i].finalEvalProof != nil { + frToBigInts(outs[offset:], p[i].finalEvalProof) + offset += len(p[i].finalEvalProof) + } + } + if offset != len(outs) { + return fmt.Errorf("expected %d elements, got %d", offset, len(outs)) + } + return nil +} + +func frToBigInts(dst []*big.Int, src []fr.Element) { + for i := range src { + src[i].BigInt(dst[i]) + } +} + +// gateAPI implements gkr.GateAPI. +type gateAPI struct{} + +var api gateAPI + +func (gateAPI) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element // TODO Heap allocated. Keep an eye on perf + res.Add(cast(i1), cast(i2)) + for _, v := range in { + res.Add(&res, cast(v)) + } + return &res +} + +func (gateAPI) MulAcc(a, b, c frontend.Variable) frontend.Variable { + var prod fr.Element + prod.Add(cast(b), cast(c)) + res := cast(a) + res.Add(res, &prod) + return &res +} + +func (gateAPI) Neg(i1 frontend.Variable) frontend.Variable { + var res fr.Element + res.Neg(cast(i1)) + return &res +} + +func (gateAPI) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Sub(cast(i1), cast(i2)) + for _, v := range in { + res.Sub(&res, cast(v)) + } + return &res +} + +func (gateAPI) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Mul(cast(i1), cast(i2)) + for _, v := range in { + res.Mul(&res, cast(v)) + } + return &res +} + +func (gateAPI) Println(a ...frontend.Variable) { + toPrint := make([]any, len(a)) + var x fr.Element + + for i, v := range a { + if _, err := x.SetInterface(v); err != nil { + toPrint[i] = x.String() + } else { + if s, ok := v.(string); ok { + toPrint[i] = s + continue + } + panic(fmt.Errorf("not numeric or string: %w", err)) + } + } + fmt.Println(toPrint...) +} + +func (api gateAPI) evaluate(f gkr.GateFunction, in ...fr.Element) *fr.Element { + inVar := make([]frontend.Variable, len(in)) + for i := range in { + inVar[i] = &in[i] + } + return f(api, inVar...).(*fr.Element) +} + +type gateFunctionFr func(...fr.Element) *fr.Element + +// convertFunc turns f into a function that accepts and returns fr.Element. +func (api gateAPI) convertFunc(f gkr.GateFunction) gateFunctionFr { + return func(in ...fr.Element) *fr.Element { + return api.evaluate(f, in...) + } +} + +func cast(v frontend.Variable) *fr.Element { + if x, ok := v.(*fr.Element); ok { // fast path, no extra heap allocation + return x + } + var x fr.Element + if _, err := x.SetInterface(v); err != nil { + panic(err) + } + return &x +} diff --git a/internal/gkr/bls12-377/gkr_test.go b/internal/gkr/bls12-377/gkr_test.go new file mode 100644 index 00000000..248a12d8 --- /dev/null +++ b/internal/gkr/bls12-377/gkr_test.go @@ -0,0 +1,505 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "os" + "path/filepath" + "reflect" + "strconv" + "testing" + "time" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/mimc" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + gcUtils "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/internal/utils" + "github.com/consensys/gnark/std/gkrapi/gkr" + "github.com/stretchr/testify/assert" +) + +func TestNoGateTwoInstances(t *testing.T) { + // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case + testNoGate(t, []fr.Element{four, three}) +} + +func TestNoGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}}) +} + +func TestSingleAddGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Add2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleMulGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Mul2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleInputTwoIdentityGates(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + }) +} + +func TestSingleInputTwoIdentityGatesComposed(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{1}, + }}) +} + +func TestAPowNTimesBCircuit(t *testing.T) { + const N = 10 + + c := make(gkrtypes.Circuit, N+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: gkrtypes.Mul2(), + Inputs: []int{i - 1, 0}, + } + } + + test(t, c) +} + +func TestSingleMimcCipherGate(t *testing.T) { + test(t, gkrtypes.Circuit{ + {}, {}, + { + Inputs: []int{0, 1}, + Gate: cache.GetGate("mimc"), + }, + }) +} + +func TestShallowMimcTwoInstances(t *testing.T) { + test(t, mimcCircuit(2)) +} + +func TestMimc(t *testing.T) { + test(t, mimcCircuit(93)) +} + +func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { + circuit := gkrtypes.Circuit{gkrtypes.Wire{ + Gate: gkrtypes.Identity(), + NbUniqueOutputs: 2, + }} + + assignment := WireAssignment{[]fr.Element{two, three}} + var o settings + pool := polynomial.NewPool(256, 1<<11) + workers := gcUtils.NewWorkerPool() + o.pool = &pool + o.workers = workers + + claimsManagerGen := func() *claimsManager { + manager := newClaimsManager(utils.References(circuit), assignment, o) + manager.add(0, []fr.Element{three}, five) + manager.add(0, []fr.Element{four}, six) + return &manager + } + + transcriptGen := newMessageCounterGenerator(4, 1) + + proof, err := sumcheckProve(claimsManagerGen().getClaim(0), fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) + err = sumcheckVerify(claimsManagerGen().getLazyClaim(0), proof, fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) +} + +var one, two, three, four, five, six fr.Element + +func init() { + one.SetOne() + two.Double(&one) + three.Add(&two, &one) + four.Double(&two) + five.Add(&three, &two) + six.Double(&three) +} + +var testManyInstancesLogMaxInstances = -1 + +func getLogMaxInstances(t *testing.T) int { + if testManyInstancesLogMaxInstances == -1 { + + s := os.Getenv("GKR_LOG_INSTANCES") + if s == "" { + testManyInstancesLogMaxInstances = 5 + } else { + var err error + testManyInstancesLogMaxInstances, err = strconv.Atoi(s) + if err != nil { + t.Error(err) + } + } + + } + return testManyInstancesLogMaxInstances +} + +func test(t *testing.T, circuit gkrtypes.Circuit) { + wireRefs := utils.References(circuit) + ins := circuit.Inputs() + insAssignment := make(WireAssignment, len(ins)) + maxSize := 1 << getLogMaxInstances(t) + + for i := range ins { + insAssignment[i] = make([]fr.Element, maxSize) + fr.Vector(insAssignment[i]).MustSetRandom() + } + + fullAssignment := make(WireAssignment, len(circuit)) + for _, numEvals := range []int{2, maxSize} { + for i := range ins { + fullAssignment[ins[i]] = insAssignment[i][:numEvals] + } + + fullAssignment.Complete(wireRefs) + + t.Log("Selected inputs for test") + + proof, err := Prove(circuit, fullAssignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") + + if proof.isEmpty() { // special case for TestNoGate: + continue // there's no way to make a trivial proof fail + } + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(0, 1))) + assert.NotNil(t, err, "bad proof accepted") + } + +} + +func (p Proof) isEmpty() bool { + for i := range p { + if len(p[i].finalEvalProof) != 0 { + return false + } + for j := range p[i].partialSumPolys { + if len(p[i].partialSumPolys[j]) != 0 { + return false + } + } + } + return true +} + +func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { + c := gkrtypes.Circuit{ + {}, + } + + assignment := WireAssignment{0: inputAssignments[0]} + + proof, err := Prove(c, assignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(c, assignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") +} + +func mimcCircuit(numRounds int) gkrtypes.Circuit { + c := make(gkrtypes.Circuit, numRounds+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: cache.GetGate("mimc"), + Inputs: []int{i - 1, 0}, + } + } + return c +} + +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("bivariate input needed") + } + res := api.Add(x[0], x[1]) + return api.Mul(res, x[0]) + } + + // g: x,y -> x² + 3y + g := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + res := api.Mul(x[0], x[0]) + y3 := api.Mul(x[1], 3) + return api.Add(res, y3) + } + + // h: x -> 2x + // but it edits it input + h := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + return api.Add(x[0], x[0]) + } + + assert.False(t, IsGateFunctionAdditive(f, 1, 2)) + assert.False(t, IsGateFunctionAdditive(f, 0, 2)) + + assert.False(t, IsGateFunctionAdditive(g, 0, 2)) + assert.True(t, IsGateFunctionAdditive(g, 1, 2)) + + assert.True(t, IsGateFunctionAdditive(h, 0, 1)) +} + +func generateTestProver(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + proof, err := Prove(testCase.Circuit, testCase.FullAssignment, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err) + assert.NoError(t, proofEquals(testCase.Proof, proof)) + } +} + +func generateTestVerifier(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err, "proof rejected") + testCase, err = newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(newMessageCounter(2, 0))) + assert.NotNil(t, err, "bad proof accepted") + } +} + +func TestGkrVectors(t *testing.T) { + + const testDirPath = "../test_vectors/" + dirEntries, err := os.ReadDir(testDirPath) + assert.NoError(t, err) + for _, dirEntry := range dirEntries { + if !dirEntry.IsDir() { + + if filepath.Ext(dirEntry.Name()) == ".json" { + path := filepath.Join(testDirPath, dirEntry.Name()) + noExt := dirEntry.Name()[:len(dirEntry.Name())-len(".json")] + + t.Run(noExt+"_prover", generateTestProver(path)) + t.Run(noExt+"_verifier", generateTestVerifier(path)) + + } + } + } +} + +func proofEquals(expected Proof, seen Proof) error { + if len(expected) != len(seen) { + return fmt.Errorf("length mismatch %d ≠ %d", len(expected), len(seen)) + } + for i, x := range expected { + xSeen := seen[i] + + if xSeen.finalEvalProof == nil { + if seenFinalEval := x.finalEvalProof; len(seenFinalEval) != 0 { + return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) + } + } else { + if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + return fmt.Errorf("final evaluation proof mismatch") + } + } + if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + return err + } + } + return nil +} + +func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { + fmt.Println("creating circuit structure") + c := mimcCircuit(mimcDepth) + + in0 := make([]fr.Element, nbInstances) + in1 := make([]fr.Element, nbInstances) + fr.Vector(in0).MustSetRandom() + fr.Vector(in1).MustSetRandom() + + fmt.Println("evaluating circuit") + start := time.Now().UnixMicro() + assignment := WireAssignment{in0, in1}.Complete(utils.References(c)) + solved := time.Now().UnixMicro() - start + fmt.Println("solved in", solved, "μs") + + //b.ResetTimer() + fmt.Println("constructing proof") + start = time.Now().UnixMicro() + _, err := Prove(c, assignment, fiatshamir.WithHash(mimc.NewMiMC())) + proved := time.Now().UnixMicro() - start + fmt.Println("proved in", proved, "μs") + assert.NoError(b, err) +} + +func BenchmarkGkrMimc19(b *testing.B) { + benchmarkGkrMiMC(b, 1<<19, 91) +} + +func BenchmarkGkrMimc17(b *testing.B) { + benchmarkGkrMiMC(b, 1<<17, 91) +} + +func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { + proof := make(Proof, len(printable)) + for i := range printable { + finalEvalProof := []fr.Element(nil) + + if printable[i].FinalEvalProof != nil { + finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) + finalEvalProof = make([]fr.Element, finalEvalSlice.Len()) + for k := range finalEvalProof { + if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + return nil, err + } + } + } + + proof[i] = sumcheckProof{ + partialSumPolys: make([]polynomial.Polynomial, len(printable[i].PartialSumPolys)), + finalEvalProof: finalEvalProof, + } + for k := range printable[i].PartialSumPolys { + var err error + if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + return nil, err + } + } + } + return proof, nil +} + +type TestCase struct { + Circuit gkrtypes.Circuit + Hash hash.Hash + Proof Proof + FullAssignment WireAssignment + InOutAssignment WireAssignment +} + +var ( + testCases = make(map[string]*TestCase) + cache = gkrtesting.NewCache() +) + +func newTestCase(path string) (*TestCase, error) { + path, err := filepath.Abs(path) + if err != nil { + return nil, err + } + dir := filepath.Dir(path) + + tCase, ok := testCases[path] + if ok { + return tCase, nil + } + + info, err := cache.ReadTestCaseInfo(path) + if err != nil { + return nil, err + } + + circuit := cache.GetCircuit(filepath.Join(dir, info.Circuit)) + var _hash hash.Hash + if _hash, err = hashFromDescription(info.Hash); err != nil { + return nil, err + } + var proof Proof + if proof, err = unmarshalProof(info.Proof); err != nil { + return nil, err + } + + fullAssignment := make(WireAssignment, len(circuit)) + inOutAssignment := make(WireAssignment, len(circuit)) + + sorted := circuit.TopologicalSort() + + inI, outI := 0, 0 + for i, w := range sorted { + var assignmentRaw []interface{} + if w.IsInput() { + if inI == len(info.Input) { + return nil, fmt.Errorf("fewer input in vector than in circuit") + } + assignmentRaw = info.Input[inI] + inI++ + } else if w.IsOutput() { + if outI == len(info.Output) { + return nil, fmt.Errorf("fewer output in vector than in circuit") + } + assignmentRaw = info.Output[outI] + outI++ + } + if assignmentRaw != nil { + var wireAssignment []fr.Element + if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + return nil, err + } + + fullAssignment[i] = wireAssignment + inOutAssignment[i] = wireAssignment + } + } + + fullAssignment.Complete(utils.References(circuit)) + + for i, w := range sorted { + if w.IsOutput() { + if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + return nil, fmt.Errorf("assignment mismatch: %v", err) + } + } + } + + tCase = &TestCase{ + FullAssignment: fullAssignment, + InOutAssignment: inOutAssignment, + Proof: proof, + Hash: _hash, + Circuit: circuit, + } + + testCases[path] = tCase + + return tCase, nil +} diff --git a/internal/gkr/bls12-377/solver_hints.go b/internal/gkr/bls12-377/solver_hints.go new file mode 100644 index 00000000..39547cff --- /dev/null +++ b/internal/gkr/bls12-377/solver_hints.go @@ -0,0 +1,146 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark-crypto/hash" + "github.com/consensys/gnark-crypto/utils" + hint "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + algo_utils "github.com/consensys/gnark/internal/utils" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +type SolvingData struct { + assignment WireAssignment + circuit gkrtypes.Circuit + workers *utils.WorkerPool +} + +func (d *SolvingData) init(info gkrtypes.SolvingInfo) { + d.workers = utils.NewWorkerPool() + d.circuit = info.Circuit + d.circuit.SetNbUniqueOutputs() + + d.assignment = make(WireAssignment, len(d.circuit)) + for i := range d.assignment { + d.assignment[i] = make([]fr.Element, info.NbInstances) + } +} + +// this module assumes that wire and instance indexes respect dependencies + +func setOuts(a WireAssignment, circuit gkrtypes.Circuit, outs []*big.Int) { + outsI := 0 + for i := range circuit { + if circuit[i].IsOutput() { + for j := range a[i] { + a[i][j].BigInt(outs[outsI]) + outsI++ + } + } + } + // Check if outsI == len(outs)? +} + +func SolveHint(info gkrtypes.SolvingInfo, data *SolvingData) hint.Hint { + return func(_ *big.Int, ins, outs []*big.Int) error { + // assumes assignmentVector is arranged wire first, instance second in order of solution + offsets := info.AssignmentOffsets() + data.init(info) + maxNIn := data.circuit.MaxGateNbIn() + + chunks := info.Chunks() + + solveTask := func(chunkOffset int) utils.Task { + return func(startInChunk, endInChunk int) { + start := startInChunk + chunkOffset + end := endInChunk + chunkOffset + inputs := make([]frontend.Variable, maxNIn) + dependencyHeads := make([]int, len(data.circuit)) // for each wire, which of its dependencies we would look at next + for wI := range data.circuit { // skip instances that are not relevant (related to instances before the current task) + deps := info.Dependencies[wI] + dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { + return deps[i].InputInstance + }, len(deps), start) + } + + for instanceI := start; instanceI < end; instanceI++ { + for wireI := range data.circuit { + wire := &data.circuit[wireI] + deps := info.Dependencies[wireI] + if wire.IsInput() { + if dependencyHeads[wireI] < len(deps) && instanceI == deps[dependencyHeads[wireI]].InputInstance { + dep := deps[dependencyHeads[wireI]] + data.assignment[wireI][instanceI].Set(&data.assignment[dep.OutputWire][dep.OutputInstance]) + dependencyHeads[wireI]++ + } else { + data.assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) + } + } else { + // assemble the inputs + inputIndexes := info.Circuit[wireI].Inputs + for i, inputI := range inputIndexes { + inputs[i] = &data.assignment[inputI][instanceI] + } + gate := data.circuit[wireI].Gate + data.assignment[wireI][instanceI].Set(gate.Evaluate(api, inputs[:len(inputIndexes)]...).(*fr.Element)) + } + } + } + } + } + + start := 0 + for _, end := range chunks { + data.workers.Submit(end-start, solveTask(start), 1024).Wait() + start = end + } + + for _, p := range info.Prints { + serializable := make([]any, len(p.Values)) + for i, v := range p.Values { + if p.IsGkrVar[i] { // serializer stores uint32 in slices as uint64 + serializable[i] = data.assignment[algo_utils.ForceUint32(v)][p.Instance].String() + } else { + serializable[i] = v + } + } + fmt.Println(serializable...) + } + + setOuts(data.assignment, info.Circuit, outs) + + return nil + } +} + +func ProveHint(hashName string, data *SolvingData) hint.Hint { + + return func(_ *big.Int, ins, outs []*big.Int) error { + insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called + b := make([]byte, fr.Bytes) + i.FillBytes(b) + return b[:] + }) + + hsh := hash.NewHash(hashName + "_BLS12_377") + + proof, err := Prove(data.circuit, data.assignment, fiatshamir.WithHash(hsh, insBytes...), WithWorkers(data.workers)) + if err != nil { + return err + } + + return proof.SerializeToBigInts(outs) + + } +} diff --git a/internal/gkr/bls12-377/sumcheck.go b/internal/gkr/bls12-377/sumcheck.go new file mode 100644 index 00000000..4fe888a9 --- /dev/null +++ b/internal/gkr/bls12-377/sumcheck.go @@ -0,0 +1,171 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "strconv" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +// This does not make use of parallelism and represents polynomials as lists of coefficients +// It is currently geared towards arithmetic hashes. Once we have a more unified hash function interface, this can be generified. + +// sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. +// Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) +type sumcheckClaims interface { + combine(a fr.Element) polynomial.Polynomial // combine into the 0ᵗʰ sumcheck subclaim. Create g := ∑_{1≤j≤m} aʲ⁻¹fⱼ for which now we seek to prove ∑_{0≤i<2ⁿ} g(i) = c := ∑_{1≤j≤m} aʲ⁻¹cⱼ. Return g₁. + next(fr.Element) polynomial.Polynomial // Return the evaluations gⱼ(k) for 1 ≤ k < degⱼ(g). Update the claim to gⱼ₊₁ for the input value as rⱼ + varsNum() int // number of variables + claimsNum() int // number of claims + proveFinalEval(r []fr.Element) []fr.Element // in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +// sumcheckLazyClaims is the sumcheckClaims data structure on the verifier side. It is "lazy" in that it has to compute fewer things. +type sumcheckLazyClaims interface { + claimsNum() int // claimsNum = m + varsNum() int // varsNum = n + combinedSum(a fr.Element) fr.Element // combinedSum returns c = ∑_{1≤j≤m} aʲ⁻¹cⱼ + degree(i int) int // degree of the total claim in the i'th variable + verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error +} + +// sumcheckProof of a multi-statement. +type sumcheckProof struct { + partialSumPolys []polynomial.Polynomial + finalEvalProof []fr.Element //in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +func setupTranscript(claimsNum int, varsNum int, settings *fiatshamir.Settings) (challengeNames []string, err error) { + numChallenges := varsNum + if claimsNum >= 2 { + numChallenges++ + } + challengeNames = make([]string, numChallenges) + if claimsNum >= 2 { + challengeNames[0] = settings.Prefix + "comb" + } + prefix := settings.Prefix + "pSP." + for i := 0; i < varsNum; i++ { + challengeNames[i+numChallenges-varsNum] = prefix + strconv.Itoa(i) + } + if settings.Transcript == nil { + transcript := fiatshamir.NewTranscript(settings.Hash, challengeNames...) + settings.Transcript = transcript + } + + for i := range settings.BaseChallenges { + if err = settings.Transcript.Bind(challengeNames[0], settings.BaseChallenges[i]); err != nil { + return + } + } + return +} + +func next(transcript *fiatshamir.Transcript, bindings []fr.Element, remainingChallengeNames *[]string) (fr.Element, error) { + challengeName := (*remainingChallengeNames)[0] + for i := range bindings { + bytes := bindings[i].Bytes() + if err := transcript.Bind(challengeName, bytes[:]); err != nil { + return fr.Element{}, err + } + } + var res fr.Element + bytes, err := transcript.ComputeChallenge(challengeName) + res.SetBytes(bytes) + + *remainingChallengeNames = (*remainingChallengeNames)[1:] + + return res, err +} + +// sumcheckProve create a non-interactive proof +func sumcheckProve(claims sumcheckClaims, transcriptSettings fiatshamir.Settings) (sumcheckProof, error) { + + var proof sumcheckProof + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return proof, err + } + + var combinationCoeff fr.Element + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return proof, err + } + } + + varsNum := claims.varsNum() + proof.partialSumPolys = make([]polynomial.Polynomial, varsNum) + proof.partialSumPolys[0] = claims.combine(combinationCoeff) + challenges := make([]fr.Element, varsNum) + + for j := 0; j+1 < varsNum; j++ { + if challenges[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return proof, err + } + proof.partialSumPolys[j+1] = claims.next(challenges[j]) + } + + if challenges[varsNum-1], err = next(transcript, proof.partialSumPolys[varsNum-1], &remainingChallengeNames); err != nil { + return proof, err + } + + proof.finalEvalProof = claims.proveFinalEval(challenges) + + return proof, nil +} + +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, transcriptSettings fiatshamir.Settings) error { + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return err + } + + var combinationCoeff fr.Element + + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return err + } + } + + r := make([]fr.Element, claims.varsNum()) + + // Just so that there is enough room for gJ to be reused + maxDegree := claims.degree(0) + for j := 1; j < claims.varsNum(); j++ { + if d := claims.degree(j); d > maxDegree { + maxDegree = d + } + } + gJ := make(polynomial.Polynomial, maxDegree+1) //At the end of iteration j, gJ = ∑_{i < 2ⁿ⁻ʲ⁻¹} g(X₁, ..., Xⱼ₊₁, i...) NOTE: n is shorthand for claims.varsNum() + gJR := claims.combinedSum(combinationCoeff) // At the beginning of iteration j, gJR = ∑_{i < 2ⁿ⁻ʲ} g(r₁, ..., rⱼ, i...) + + for j := range claims.varsNum() { + if len(proof.partialSumPolys[j]) != claims.degree(j) { + return errors.New("malformed proof") + } + copy(gJ[1:], proof.partialSumPolys[j]) + gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) // Requirement that gⱼ(0) + gⱼ(1) = gⱼ₋₁(r) + // gJ is ready + + //Prepare for the next iteration + if r[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return err + } + // This is an extremely inefficient way of interpolating. TODO: Interpolate without symbolically computing a polynomial + gJCoeffs := polynomial.InterpolateOnRange(gJ[:(claims.degree(j) + 1)]) + gJR = gJCoeffs.Eval(&r[j]) + } + + return claims.verifyFinalEval(r, combinationCoeff, gJR, proof.finalEvalProof) +} diff --git a/internal/gkr/bls12-377/sumcheck_test.go b/internal/gkr/bls12-377/sumcheck_test.go new file mode 100644 index 00000000..ffaae88a --- /dev/null +++ b/internal/gkr/bls12-377/sumcheck_test.go @@ -0,0 +1,152 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/stretchr/testify/assert" + + "math/bits" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + + "strings" + "testing" +) + +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { + poly := make(polynomial.MultiLin, len(polyInt)) + for i, n := range polyInt { + poly[i].SetUint64(n) + } + + claim := singleMultilinClaim{g: poly.Clone()} + + proof, err := sumcheckProve(&claim, fiatshamir.WithHash(hashGenerator())) + if err != nil { + return err + } + + var sb strings.Builder + for _, p := range proof.partialSumPolys { + + sb.WriteString("\t{") + for i := 0; i < len(p); i++ { + sb.WriteString(p[i].String()) + if i+1 < len(p) { + sb.WriteString(", ") + } + } + sb.WriteString("}\n") + } + + lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if err = sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())); err != nil { + return err + } + + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())) == nil { + return fmt.Errorf("bad proof accepted") + } + return nil +} + +func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { + + polys := [][]uint64{ + {1, 2, 3, 4}, // 1 + 2X₁ + X₂ + {1, 2, 3, 4, 5, 6, 7, 8}, // 1 + 4X₁ + 2X₂ + X₃ + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, // 1 + 8X₁ + 4X₂ + 2X₃ + X₄ + } + + const MaxStep = 4 + const MaxStart = 4 + hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + + for step := 0; step < MaxStep; step++ { + for startState := 0; startState < MaxStart; startState++ { + if step == 0 && startState == 1 { // unlucky case where a bad proof would be accepted + continue + } + hashGens = append(hashGens, newMessageCounterGenerator(startState, step)) + } + } + + for _, poly := range polys { + for _, hashGen := range hashGens { + assert.NoError(t, testSumcheckSingleClaimMultilin(poly, hashGen), + "failed with poly %v and hashGen %v", poly, hashGen()) + } + } +} + +type singleMultilinClaim struct { + g polynomial.MultiLin +} + +func (c singleMultilinClaim) proveFinalEval(r []fr.Element) []fr.Element { + return nil // verifier can compute the final eval itself +} + +func (c singleMultilinClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} + +func (c singleMultilinClaim) claimsNum() int { + return 1 +} + +func sumForX1One(g polynomial.MultiLin) polynomial.Polynomial { + sum := g[len(g)/2] + for i := len(g)/2 + 1; i < len(g); i++ { + sum.Add(&sum, &g[i]) + } + return []fr.Element{sum} +} + +func (c singleMultilinClaim) combine(fr.Element) polynomial.Polynomial { + return sumForX1One(c.g) +} + +func (c *singleMultilinClaim) next(r fr.Element) polynomial.Polynomial { + c.g.Fold(r) + return sumForX1One(c.g) +} + +type singleMultilinLazyClaim struct { + g polynomial.MultiLin + claimedSum fr.Element +} + +func (c singleMultilinLazyClaim) verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error { + val := c.g.Evaluate(r, nil) + if val.Equal(&purportedValue) { + return nil + } + return fmt.Errorf("mismatch") +} + +func (c singleMultilinLazyClaim) combinedSum(combinationCoeffs fr.Element) fr.Element { + return c.claimedSum +} + +func (c singleMultilinLazyClaim) degree(i int) int { + return 1 +} + +func (c singleMultilinLazyClaim) claimsNum() int { + return 1 +} + +func (c singleMultilinLazyClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} diff --git a/internal/gkr/bls12-377/test_vector_utils_test.go b/internal/gkr/bls12-377/test_vector_utils_test.go new file mode 100644 index 00000000..e2dd11d4 --- /dev/null +++ b/internal/gkr/bls12-377/test_vector_utils_test.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "strings" + + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrtesting" +) + +func toElement(i int64) *fr.Element { + var res fr.Element + res.SetInt64(i) + return &res +} + +func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { + if _type, ok := d["type"]; ok { + switch _type { + case "const": + startState := int64(d["val"].(float64)) + return &messageCounter{startState: startState, step: 0, state: startState}, nil + default: + return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) + } + } + return nil, fmt.Errorf("hash description missing type") +} + +type messageCounter struct { + startState int64 + state int64 + step int64 +} + +func (m *messageCounter) Write(p []byte) (n int, err error) { + inputBlockSize := (len(p)-1)/fr.Bytes + 1 + m.state += int64(inputBlockSize) * m.step + return len(p), nil +} + +func (m *messageCounter) Sum(b []byte) []byte { + inputBlockSize := (len(b)-1)/fr.Bytes + 1 + resI := m.state + int64(inputBlockSize)*m.step + var res fr.Element + res.SetInt64(int64(resI)) + resBytes := res.Bytes() + return resBytes[:] +} + +func (m *messageCounter) Reset() { + m.state = m.startState +} + +func (m *messageCounter) Size() int { + return fr.Bytes +} + +func (m *messageCounter) BlockSize() int { + return fr.Bytes +} + +func newMessageCounter(startState, step int) hash.Hash { + transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} + return transcript +} + +func newMessageCounterGenerator(startState, step int) func() hash.Hash { + return func() hash.Hash { + return newMessageCounter(startState, step) + } +} + +func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { + + // TODO: Put this in element.SetString? + switch v := value.(type) { + case string: + + if sep := strings.Split(v, "/"); len(sep) == 2 { + var denom fr.Element + if _, err := z.SetString(sep[0]); err != nil { + return nil, err + } + if _, err := denom.SetString(sep[1]); err != nil { + return nil, err + } + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil + } + + case float64: + asInt := int64(v) + if float64(asInt) != v { + return nil, fmt.Errorf("cannot currently parse float") + } + z.SetInt64(asInt) + return z, nil + } + + return z.SetInterface(value) +} + +func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { + elementSlice := make([]fr.Element, len(slice)) + for i, v := range slice { + if _, err := setElement(&elementSlice[i], v); err != nil { + return nil, err + } + } + return elementSlice, nil +} + +func sliceEquals(a []fr.Element, b []fr.Element) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if !a[i].Equal(&b[i]) { + return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) + } + } + return nil +} + +func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if err := sliceEquals(a[i], b[i]); err != nil { + return fmt.Errorf("at index %d: %w", i, err) + } + } + return nil +} diff --git a/internal/gkr/bls12-381/gate_testing.go b/internal/gkr/bls12-381/gate_testing.go new file mode 100644 index 00000000..ef7694dc --- /dev/null +++ b/internal/gkr/bls12-381/gate_testing.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + + "slices" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// IsGateFunctionAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func IsGateFunctionAdditive(f gkr.GateFunction, i, nbIn int) bool { + fWrapped := api.convertFunc(f) + + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 := fWrapped(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := fWrapped(in...) + + y2.Sub(y2, y1) + y1.Sub(y1, y0) + + if !y2.Equal(y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 = fWrapped(in...) + + y1.Sub(y1, y0) + + return y1.Equal(y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f gateFunctionFr) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i].Set(f(fIn...)) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +// FindGateFunctionDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func FindGateFunctionDegree(f gkr.GateFunction, max, nbIn int) (int, error) { + fFr := api.convertFunc(f) + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := fFr.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, gkrtypes.ErrZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func VerifyGateFunctionDegree(f gkr.GateFunction, claimedDegree, nbIn int) error { + fFr := api.convertFunc(f) + if p := fFr.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return gkrtypes.ErrZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} diff --git a/internal/gkr/bls12-381/gkr.go b/internal/gkr/bls12-381/gkr.go new file mode 100644 index 00000000..f5617a59 --- /dev/null +++ b/internal/gkr/bls12-381/gkr.go @@ -0,0 +1,816 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "sync" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// The goal is to prove/verify evaluations of many instances of the same circuit + +// WireAssignment is assignment of values to the same wire across many instances of the circuit +type WireAssignment []polynomial.MultiLin + +type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) + +// eqTimesGateEvalSumcheckLazyClaims is a lazy claim for sumcheck (verifier side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the checking of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckLazyClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ), allegedly + manager *claimsManager // WARNING: Circular references +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) getWire() *gkrtypes.Wire { + return e.manager.wires[e.wireI] +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) claimsNum() int { + return len(e.evaluationPoints) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) varsNum() int { + return len(e.evaluationPoints[0]) +} + +// combinedSum returns ∑ᵢ aⁱ yᵢ +func (e *eqTimesGateEvalSumcheckLazyClaims) combinedSum(a fr.Element) fr.Element { + evalsAsPoly := polynomial.Polynomial(e.claimedEvaluations) + return evalsAsPoly.Eval(&a) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) degree(int) int { + return 1 + e.manager.wires[e.wireI].Gate.Degree() +} + +// verifyFinalEval finalizes the verification of w. +// The prover's claims w(xᵢ) = yᵢ have already been reduced to verifying +// ∑ cⁱ eq(xᵢ, r) w(r) = purportedValue. ( c is combinationCoeff ) +// Both purportedValue and the vector r have been randomized during the sumcheck protocol. +// By taking the w term out of the sum we get the equivalent claim that +// for E := ∑ eq(xᵢ, r), it must be that E w(r) = purportedValue. +// If w is an input wire, the verifier can directly check its evaluation at r. +// Otherwise, the prover makes claims about the evaluation of w's input wires, +// wᵢ, at r, to be verified later. +// The claims are communicated through the proof parameter. +// The verifier checks here if the claimed evaluations of wᵢ(r) are consistent with +// the main claim, by checking E w(wᵢ(r)...) = purportedValue. +func (e *eqTimesGateEvalSumcheckLazyClaims) verifyFinalEval(r []fr.Element, combinationCoeff, purportedValue fr.Element, uniqueInputEvaluations []fr.Element) error { + // the eq terms ( E ) + numClaims := len(e.evaluationPoints) + evaluation := polynomial.EvalEq(e.evaluationPoints[numClaims-1], r) + for i := numClaims - 2; i >= 0; i-- { + evaluation.Mul(&evaluation, &combinationCoeff) + eq := polynomial.EvalEq(e.evaluationPoints[i], r) + evaluation.Add(&evaluation, &eq) + } + + wire := e.manager.wires[e.wireI] + + // the w(...) term + var gateEvaluation fr.Element + if wire.IsInput() { // just compute w(r) + gateEvaluation = e.manager.assignment[e.wireI].Evaluate(r, e.manager.memPool) + } else { // proof contains the evaluations of the inputs, but avoids repetition in case multiple inputs come from the same wire + injection, injectionLeftInv := + e.manager.wires.ClaimPropagationInfo(e.wireI) + + if len(injection) != len(uniqueInputEvaluations) { + return fmt.Errorf("%d input wire evaluations given, %d expected", len(uniqueInputEvaluations), len(injection)) + } + + for uniqueI, i := range injection { // map from unique to all + e.manager.add(wire.Inputs[i], r, uniqueInputEvaluations[uniqueI]) + } + + inputEvaluations := make([]frontend.Variable, len(wire.Inputs)) + for i, uniqueI := range injectionLeftInv { // map from all to unique + inputEvaluations[i] = &uniqueInputEvaluations[uniqueI] + } + + gateEvaluation.Set(wire.Gate.Evaluate(api, inputEvaluations...).(*fr.Element)) + } + + evaluation.Mul(&evaluation, &gateEvaluation) + + if evaluation.Equal(&purportedValue) { + return nil + } + return errors.New("incompatible evaluations") +} + +// eqTimesGateEvalSumcheckClaims is a claim for sumcheck (prover side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the proving of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ) + manager *claimsManager + + input []polynomial.MultiLin // input[i](h₁, ..., hₘ₋ⱼ) = wᵢ(r₁, r₂, ..., rⱼ₋₁, h₁, ..., hₘ₋ⱼ) + + eq polynomial.MultiLin // E := ∑ᵢ cⁱ eq(xᵢ, -) +} + +func (c *eqTimesGateEvalSumcheckClaims) getWire() *gkrtypes.Wire { + return c.manager.wires[c.wireI] +} + +// combine the multiple claims into one claim using a random combination (combinationCoeff or c). +// From the original multiple claims of w(xᵢ) = yᵢ, we get a single claim +// ∑ᵢ,ₕ cⁱ eq(xᵢ, h) w(h) = ∑ᵢ cⁱ yᵢ, where h iterates over the hypercube (circuit instances) and +// i iterates over the claims. +// Equivalently, we could say ∑ᵢ cⁱ yᵢ = ∑ₕ,ᵢ cⁱ eq(xᵢ, h) w(h) = ∑ₕ w(h) ∑ᵢ cⁱ eq(xᵢ, h). +// Thus if we initially compute E := ∑ᵢ cⁱ eq(xᵢ, -), our claim will find the simpler form +// ∑ᵢ cⁱ yᵢ = ∑ₕ w(h) E(h), where the sum-checked polynomial is of degree deg(g) + 1, +// and deg(g) is the total degree of the polynomial defining the gate g of which w is the output. +// The output of combine is the first sumcheck claim, i.e. ∑₍ₕ₁,ₕ₂,...₎ w(X, h₁, h₂, ...) E(X, h₁, h₂, ...).. +func (c *eqTimesGateEvalSumcheckClaims) combine(combinationCoeff fr.Element) polynomial.Polynomial { + varsNum := c.varsNum() + eqLength := 1 << varsNum + claimsNum := c.claimsNum() + // initialize the eq tables ( E ) + c.eq = c.manager.memPool.Make(eqLength) + + c.eq[0].SetOne() + c.eq.Eq(c.evaluationPoints[0]) + + // E := eq(x₀, -) + newEq := polynomial.MultiLin(c.manager.memPool.Make(eqLength)) + aI := combinationCoeff + + // E += cⁱ eq(xᵢ, -) + for k := 1; k < claimsNum; k++ { + newEq[0].Set(&aI) + + c.eqAcc(c.eq, newEq, c.evaluationPoints[k]) + + if k+1 < claimsNum { + aI.Mul(&aI, &combinationCoeff) + } + } + + c.manager.memPool.Dump(newEq) + + return c.computeGJ() +} + +// eqAcc sets m to an eq table at q and then adds it to e. +// m <- eq(q, -). +// e <- e + m +func (c *eqTimesGateEvalSumcheckClaims) eqAcc(e, m polynomial.MultiLin, q []fr.Element) { + n := len(q) + + //At the end of each iteration, m(h₁, ..., hₙ) = eq(q₁, ..., qᵢ₊₁, h₁, ..., hᵢ₊₁) + for i := range q { // In the comments we use a 1-based index so q[i] = qᵢ₊₁ + // go through all assignments of (b₁, ..., bᵢ) ∈ {0,1}ⁱ + const threshold = 1 << 6 + k := 1 << i + if k < threshold { + for j := 0; j < k; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + } else { + c.manager.workers.Submit(k, func(start, end int) { + for j := start; j < end; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + }, 1024).Wait() + } + + } + c.manager.workers.Submit(len(e), func(start, end int) { + for i := start; i < end; i++ { + e[i].Add(&e[i], &m[i]) + } + }, 512).Wait() +} + +// computeGJ: gⱼ = ∑_{0≤h<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, h...) = ∑_{0≤i<2ⁿ⁻ʲ} E(r₁, ..., Xⱼ, h...) g( w₀(r₁, ..., Xⱼ, h...), ... ). +// the polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). +// The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). By convention, g₀ is a constant polynomial equal to the claimed sum. +func (c *eqTimesGateEvalSumcheckClaims) computeGJ() polynomial.Polynomial { + + wire := c.getWire() + degGJ := 1 + wire.Gate.Degree() // guaranteed to be no smaller than the actual deg(gⱼ) + nbGateIn := len(c.input) + + // Both E and wᵢ (the input wires and the eq table) are multilinear, thus + // they are linear in Xⱼ. + // So for f ∈ { E(r₁, ..., Xⱼ, h...) } ∪ {wᵢ(r₁, ..., Xⱼ, h...) }, so f(m) = m×(f(1) - f(0)) + f(0), and f(0), f(1) are easily computed from the bookkeeping tables. + // ml are such multilinear polynomials the evaluations of which over different values of Xⱼ are computed in this stepwise manner. + ml := make([]polynomial.MultiLin, nbGateIn+1) // shortcut to the evaluations of the multilinear polynomials over the hypercube + ml[0] = c.eq + copy(ml[1:], c.input) + + sumSize := len(c.eq) / 2 // the range of h, over which we sum + + // Perf-TODO: Collate once at claim "combination" time and not again. then, even folding can be done in one operation every time "next" is called + + gJ := make([]fr.Element, degGJ) + var mu sync.Mutex + computeAll := func(start, end int) { // compute method to allow parallelization across instances + var step fr.Element + + res := make([]fr.Element, degGJ) + + // evaluations of ml, laid out as: + // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), + // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), + // ... + // ml[0](degGJ, h...), ml[2](degGJ, h...), ..., ml[len(ml)-1](degGJ, h...) + mlEvals := make([]fr.Element, degGJ*len(ml)) + gateInput := make([]frontend.Variable, nbGateIn) + + for h := start; h < end; h++ { // h counts across instances + + evalAt1Index := sumSize + h + for k := range ml { + // d = 0 + mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1. Can be taken directly from the table. + step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for d := 1; d < degGJ; d++ { + mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + } + } + + eIndex := 0 // index for where the current eq term is + nextEIndex := len(ml) + for d := range degGJ { + for i := range gateInput { + gateInput[i] = &mlEvals[eIndex+1+i] + } + summand := wire.Gate.Evaluate(api, gateInput...).(*fr.Element) + summand.Mul(summand, &mlEvals[eIndex]) + res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + } + } + mu.Lock() + for i := range gJ { + gJ[i].Add(&gJ[i], &res[i]) // collect into the complete sum + } + mu.Unlock() + } + + const minBlockSize = 64 + + if sumSize < minBlockSize { + // no parallelization + computeAll(0, sumSize) + } else { + c.manager.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } + + return gJ +} + +// next first folds the input and E polynomials at the given verifier challenge then computes the new gⱼ. +// Thus, j <- j+1 and rⱼ = challenge. +func (c *eqTimesGateEvalSumcheckClaims) next(challenge fr.Element) polynomial.Polynomial { + const minBlockSize = 512 + n := len(c.eq) / 2 + if n < minBlockSize { + // no parallelization + for i := 0; i < len(c.input); i++ { + c.input[i].Fold(challenge) + } + c.eq.Fold(challenge) + } else { + wgs := make([]*sync.WaitGroup, len(c.input)) + for i := 0; i < len(c.input); i++ { + wgs[i] = c.manager.workers.Submit(n, c.input[i].FoldParallel(challenge), minBlockSize) + } + c.manager.workers.Submit(n, c.eq.FoldParallel(challenge), minBlockSize).Wait() + for _, wg := range wgs { + wg.Wait() + } + } + + return c.computeGJ() +} + +func (c *eqTimesGateEvalSumcheckClaims) varsNum() int { + return len(c.evaluationPoints[0]) +} + +func (c *eqTimesGateEvalSumcheckClaims) claimsNum() int { + return len(c.claimedEvaluations) +} + +// proveFinalEval provides the values wᵢ(r₁, ..., rₙ) +func (c *eqTimesGateEvalSumcheckClaims) proveFinalEval(r []fr.Element) []fr.Element { + //defer the proof, return list of claims + + injection, _ := c.manager.wires.ClaimPropagationInfo(c.wireI) // TODO @Tabaie: Instead of doing this last, we could just have fewer input in the first place; not that likely to happen with single gates, but more so with layers. + evaluations := make([]fr.Element, len(injection)) + for i, gateInputI := range injection { + wI := c.input[gateInputI] + wI.Fold(r[len(r)-1]) // We already have wᵢ(r₁, ..., rₙ₋₁, hₙ) in a table. Only one more fold required. + c.manager.add(c.getWire().Inputs[gateInputI], r, wI[0]) + evaluations[i] = wI[0] + } + + c.manager.memPool.Dump(c.claimedEvaluations, c.eq) + + return evaluations +} + +type claimsManager struct { + claims []*eqTimesGateEvalSumcheckLazyClaims + assignment WireAssignment + memPool *polynomial.Pool + workers *utils.WorkerPool + wires gkrtypes.Wires +} + +func newClaimsManager(wires []*gkrtypes.Wire, assignment WireAssignment, o settings) (manager claimsManager) { + manager.assignment = assignment + manager.claims = make([]*eqTimesGateEvalSumcheckLazyClaims, len(wires)) + manager.memPool = o.pool + manager.workers = o.workers + manager.wires = wires + + for i, wire := range wires { + + manager.claims[i] = &eqTimesGateEvalSumcheckLazyClaims{ + wireI: i, + evaluationPoints: make([][]fr.Element, 0, wire.NbClaims()), + claimedEvaluations: manager.memPool.Make(wire.NbClaims()), + manager: &manager, + } + } + return +} + +func (m *claimsManager) add(wire int, evaluationPoint []fr.Element, evaluation fr.Element) { + claim := m.claims[wire] + i := len(claim.evaluationPoints) + claim.claimedEvaluations[i] = evaluation + claim.evaluationPoints = append(claim.evaluationPoints, evaluationPoint) +} + +func (m *claimsManager) getLazyClaim(wire int) *eqTimesGateEvalSumcheckLazyClaims { + return m.claims[wire] +} + +func (m *claimsManager) getClaim(wireI int) *eqTimesGateEvalSumcheckClaims { + lazy := m.claims[wireI] + wire := m.wires[wireI] + res := &eqTimesGateEvalSumcheckClaims{ + wireI: wireI, + evaluationPoints: lazy.evaluationPoints, + claimedEvaluations: lazy.claimedEvaluations, + manager: m, + } + + if wire.IsInput() { + res.input = []polynomial.MultiLin{m.memPool.Clone(m.assignment[wireI])} + } else { + res.input = make([]polynomial.MultiLin, len(wire.Inputs)) + + for inputI, inputW := range wire.Inputs { + res.input[inputI] = m.memPool.Clone(m.assignment[inputW]) //will be edited later, so must be deep copied + } + } + return res +} + +func (m *claimsManager) deleteClaim(wire int) { + m.claims[wire].manager = nil + m.claims[wire] = nil +} + +type settings struct { + pool *polynomial.Pool + sorted []*gkrtypes.Wire + transcript *fiatshamir.Transcript + transcriptPrefix string + nbVars int + workers *utils.WorkerPool +} + +type Option func(*settings) + +func WithPool(pool *polynomial.Pool) Option { + return func(options *settings) { + options.pool = pool + } +} + +func WithSortedCircuit(sorted []*gkrtypes.Wire) Option { + return func(options *settings) { + options.sorted = sorted + } +} + +func WithWorkers(workers *utils.WorkerPool) Option { + return func(options *settings) { + options.workers = workers + } +} + +func setup(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (settings, error) { + var o settings + var err error + for _, option := range options { + option(&o) + } + + o.nbVars = assignment.NumVars() + nbInstances := assignment.NumInstances() + if 1< 1 { //combine the claims + size++ + } + size += logNbInstances // full run of sumcheck on logNbInstances variables + } + + nums := make([]string, max(len(sorted), logNbInstances)) + for i := range nums { + nums[i] = strconv.Itoa(i) + } + + challenges := make([]string, size) + + // output wire claims + firstChallengePrefix := prefix + "fC." + for j := 0; j < logNbInstances; j++ { + challenges[j] = firstChallengePrefix + nums[j] + } + j := logNbInstances + for i := len(sorted) - 1; i >= 0; i-- { + if sorted[i].NoProof() { + continue + } + wirePrefix := prefix + "w" + nums[i] + "." + + if sorted[i].NbClaims() > 1 { + challenges[j] = wirePrefix + "comb" + j++ + } + + partialSumPrefix := wirePrefix + "pSP." + for k := 0; k < logNbInstances; k++ { + challenges[j] = partialSumPrefix + nums[k] + j++ + } + } + return challenges +} + +func getFirstChallengeNames(logNbInstances int, prefix string) []string { + res := make([]string, logNbInstances) + firstChallengePrefix := prefix + "fC." + for i := 0; i < logNbInstances; i++ { + res[i] = firstChallengePrefix + strconv.Itoa(i) + } + return res +} + +func getChallenges(transcript *fiatshamir.Transcript, names []string) ([]fr.Element, error) { + res := make([]fr.Element, len(names)) + for i, name := range names { + if bytes, err := transcript.ComputeChallenge(name); err == nil { + res[i].SetBytes(bytes) + } else { + return nil, err + } + } + return res, nil +} + +// Prove consistency of the claimed assignment +func Prove(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (Proof, error) { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return nil, err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + proof := make(Proof, len(c)) + // firstChallenge called rho in the paper + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return nil, err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + claim := claims.getClaim(i) + if wire.NoProof() { // input wires with one claim only + proof[i] = sumcheckProof{ + partialSumPolys: []polynomial.Polynomial{}, + finalEvalProof: []fr.Element{}, + } + } else { + if proof[i], err = sumcheckProve( + claim, fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err != nil { + return proof, err + } + + baseChallenge = make([][]byte, len(proof[i].finalEvalProof)) + for j := range proof[i].finalEvalProof { + baseChallenge[j] = proof[i].finalEvalProof[j].Marshal() + } + } + // the verifier checks a single claim about input wires itself + claims.deleteClaim(i) + } + + return proof, nil +} + +// Verify the consistency of the claimed output with the claimed input +// Unlike in Prove, the assignment argument need not be complete +func Verify(c gkrtypes.Circuit, assignment WireAssignment, proof Proof, transcriptSettings fiatshamir.Settings, options ...Option) error { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + proofW := proof[i] + claim := claims.getLazyClaim(i) + if wire.NoProof() { // input wires with one claim only + // make sure the proof is empty + if len(proofW.finalEvalProof) != 0 || len(proofW.partialSumPolys) != 0 { + return errors.New("no proof allowed for input wire with a single claim") + } + + if wire.NbClaims() == 1 { // input wire + // simply evaluate and see if it matches + evaluation := assignment[i].Evaluate(claim.evaluationPoints[0], claims.memPool) + if !claim.claimedEvaluations[0].Equal(&evaluation) { + return errors.New("incorrect input wire claim") + } + } + } else if err = sumcheckVerify( + claim, proof[i], fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err == nil { // incorporate prover claims about w's input into the transcript + baseChallenge = make([][]byte, len(proofW.finalEvalProof)) + for j := range baseChallenge { + baseChallenge[j] = proofW.finalEvalProof[j].Marshal() + } + } else { + return fmt.Errorf("sumcheck proof rejected: %v", err) //TODO: Any polynomials to dump? + } + claims.deleteClaim(i) + } + return nil +} + +// Complete the circuit evaluation from input values +func (a WireAssignment) Complete(wires gkrtypes.Wires) WireAssignment { + + nbInstances := a.NumInstances() + maxNbIns := 0 + + for i, w := range wires { + maxNbIns = max(maxNbIns, len(w.Inputs)) + if len(a[i]) != nbInstances { + a[i] = make([]fr.Element, nbInstances) + } + } + + ins := make([]fr.Element, maxNbIns) + for i := range nbInstances { + for wI, w := range wires { + if !w.IsInput() { + for inI, in := range w.Inputs { + ins[inI] = a[in][i] + } + a[wI][i].Set(api.evaluate(w.Gate.Evaluate, ins[:len(w.Inputs)]...)) + } + } + } + + return a +} + +func (a WireAssignment) NumInstances() int { + for _, aW := range a { + return len(aW) + } + panic("empty assignment") +} + +func (a WireAssignment) NumVars() int { + for _, aW := range a { + return aW.NumVars() + } + panic("empty assignment") +} + +// SerializeToBigInts flattens a proof object into the given slice of big.Ints +// useful in gnark hints. +func (p Proof) SerializeToBigInts(outs []*big.Int) error { + offset := 0 + for i := range p { + for _, poly := range p[i].partialSumPolys { + frToBigInts(outs[offset:], poly) + offset += len(poly) + } + if p[i].finalEvalProof != nil { + frToBigInts(outs[offset:], p[i].finalEvalProof) + offset += len(p[i].finalEvalProof) + } + } + if offset != len(outs) { + return fmt.Errorf("expected %d elements, got %d", offset, len(outs)) + } + return nil +} + +func frToBigInts(dst []*big.Int, src []fr.Element) { + for i := range src { + src[i].BigInt(dst[i]) + } +} + +// gateAPI implements gkr.GateAPI. +type gateAPI struct{} + +var api gateAPI + +func (gateAPI) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element // TODO Heap allocated. Keep an eye on perf + res.Add(cast(i1), cast(i2)) + for _, v := range in { + res.Add(&res, cast(v)) + } + return &res +} + +func (gateAPI) MulAcc(a, b, c frontend.Variable) frontend.Variable { + var prod fr.Element + prod.Add(cast(b), cast(c)) + res := cast(a) + res.Add(res, &prod) + return &res +} + +func (gateAPI) Neg(i1 frontend.Variable) frontend.Variable { + var res fr.Element + res.Neg(cast(i1)) + return &res +} + +func (gateAPI) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Sub(cast(i1), cast(i2)) + for _, v := range in { + res.Sub(&res, cast(v)) + } + return &res +} + +func (gateAPI) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Mul(cast(i1), cast(i2)) + for _, v := range in { + res.Mul(&res, cast(v)) + } + return &res +} + +func (gateAPI) Println(a ...frontend.Variable) { + toPrint := make([]any, len(a)) + var x fr.Element + + for i, v := range a { + if _, err := x.SetInterface(v); err != nil { + toPrint[i] = x.String() + } else { + if s, ok := v.(string); ok { + toPrint[i] = s + continue + } + panic(fmt.Errorf("not numeric or string: %w", err)) + } + } + fmt.Println(toPrint...) +} + +func (api gateAPI) evaluate(f gkr.GateFunction, in ...fr.Element) *fr.Element { + inVar := make([]frontend.Variable, len(in)) + for i := range in { + inVar[i] = &in[i] + } + return f(api, inVar...).(*fr.Element) +} + +type gateFunctionFr func(...fr.Element) *fr.Element + +// convertFunc turns f into a function that accepts and returns fr.Element. +func (api gateAPI) convertFunc(f gkr.GateFunction) gateFunctionFr { + return func(in ...fr.Element) *fr.Element { + return api.evaluate(f, in...) + } +} + +func cast(v frontend.Variable) *fr.Element { + if x, ok := v.(*fr.Element); ok { // fast path, no extra heap allocation + return x + } + var x fr.Element + if _, err := x.SetInterface(v); err != nil { + panic(err) + } + return &x +} diff --git a/internal/gkr/bls12-381/gkr_test.go b/internal/gkr/bls12-381/gkr_test.go new file mode 100644 index 00000000..63aaa6ec --- /dev/null +++ b/internal/gkr/bls12-381/gkr_test.go @@ -0,0 +1,505 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "os" + "path/filepath" + "reflect" + "strconv" + "testing" + "time" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/mimc" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + gcUtils "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/internal/utils" + "github.com/consensys/gnark/std/gkrapi/gkr" + "github.com/stretchr/testify/assert" +) + +func TestNoGateTwoInstances(t *testing.T) { + // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case + testNoGate(t, []fr.Element{four, three}) +} + +func TestNoGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}}) +} + +func TestSingleAddGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Add2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleMulGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Mul2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleInputTwoIdentityGates(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + }) +} + +func TestSingleInputTwoIdentityGatesComposed(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{1}, + }}) +} + +func TestAPowNTimesBCircuit(t *testing.T) { + const N = 10 + + c := make(gkrtypes.Circuit, N+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: gkrtypes.Mul2(), + Inputs: []int{i - 1, 0}, + } + } + + test(t, c) +} + +func TestSingleMimcCipherGate(t *testing.T) { + test(t, gkrtypes.Circuit{ + {}, {}, + { + Inputs: []int{0, 1}, + Gate: cache.GetGate("mimc"), + }, + }) +} + +func TestShallowMimcTwoInstances(t *testing.T) { + test(t, mimcCircuit(2)) +} + +func TestMimc(t *testing.T) { + test(t, mimcCircuit(93)) +} + +func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { + circuit := gkrtypes.Circuit{gkrtypes.Wire{ + Gate: gkrtypes.Identity(), + NbUniqueOutputs: 2, + }} + + assignment := WireAssignment{[]fr.Element{two, three}} + var o settings + pool := polynomial.NewPool(256, 1<<11) + workers := gcUtils.NewWorkerPool() + o.pool = &pool + o.workers = workers + + claimsManagerGen := func() *claimsManager { + manager := newClaimsManager(utils.References(circuit), assignment, o) + manager.add(0, []fr.Element{three}, five) + manager.add(0, []fr.Element{four}, six) + return &manager + } + + transcriptGen := newMessageCounterGenerator(4, 1) + + proof, err := sumcheckProve(claimsManagerGen().getClaim(0), fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) + err = sumcheckVerify(claimsManagerGen().getLazyClaim(0), proof, fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) +} + +var one, two, three, four, five, six fr.Element + +func init() { + one.SetOne() + two.Double(&one) + three.Add(&two, &one) + four.Double(&two) + five.Add(&three, &two) + six.Double(&three) +} + +var testManyInstancesLogMaxInstances = -1 + +func getLogMaxInstances(t *testing.T) int { + if testManyInstancesLogMaxInstances == -1 { + + s := os.Getenv("GKR_LOG_INSTANCES") + if s == "" { + testManyInstancesLogMaxInstances = 5 + } else { + var err error + testManyInstancesLogMaxInstances, err = strconv.Atoi(s) + if err != nil { + t.Error(err) + } + } + + } + return testManyInstancesLogMaxInstances +} + +func test(t *testing.T, circuit gkrtypes.Circuit) { + wireRefs := utils.References(circuit) + ins := circuit.Inputs() + insAssignment := make(WireAssignment, len(ins)) + maxSize := 1 << getLogMaxInstances(t) + + for i := range ins { + insAssignment[i] = make([]fr.Element, maxSize) + fr.Vector(insAssignment[i]).MustSetRandom() + } + + fullAssignment := make(WireAssignment, len(circuit)) + for _, numEvals := range []int{2, maxSize} { + for i := range ins { + fullAssignment[ins[i]] = insAssignment[i][:numEvals] + } + + fullAssignment.Complete(wireRefs) + + t.Log("Selected inputs for test") + + proof, err := Prove(circuit, fullAssignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") + + if proof.isEmpty() { // special case for TestNoGate: + continue // there's no way to make a trivial proof fail + } + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(0, 1))) + assert.NotNil(t, err, "bad proof accepted") + } + +} + +func (p Proof) isEmpty() bool { + for i := range p { + if len(p[i].finalEvalProof) != 0 { + return false + } + for j := range p[i].partialSumPolys { + if len(p[i].partialSumPolys[j]) != 0 { + return false + } + } + } + return true +} + +func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { + c := gkrtypes.Circuit{ + {}, + } + + assignment := WireAssignment{0: inputAssignments[0]} + + proof, err := Prove(c, assignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(c, assignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") +} + +func mimcCircuit(numRounds int) gkrtypes.Circuit { + c := make(gkrtypes.Circuit, numRounds+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: cache.GetGate("mimc"), + Inputs: []int{i - 1, 0}, + } + } + return c +} + +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("bivariate input needed") + } + res := api.Add(x[0], x[1]) + return api.Mul(res, x[0]) + } + + // g: x,y -> x² + 3y + g := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + res := api.Mul(x[0], x[0]) + y3 := api.Mul(x[1], 3) + return api.Add(res, y3) + } + + // h: x -> 2x + // but it edits it input + h := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + return api.Add(x[0], x[0]) + } + + assert.False(t, IsGateFunctionAdditive(f, 1, 2)) + assert.False(t, IsGateFunctionAdditive(f, 0, 2)) + + assert.False(t, IsGateFunctionAdditive(g, 0, 2)) + assert.True(t, IsGateFunctionAdditive(g, 1, 2)) + + assert.True(t, IsGateFunctionAdditive(h, 0, 1)) +} + +func generateTestProver(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + proof, err := Prove(testCase.Circuit, testCase.FullAssignment, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err) + assert.NoError(t, proofEquals(testCase.Proof, proof)) + } +} + +func generateTestVerifier(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err, "proof rejected") + testCase, err = newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(newMessageCounter(2, 0))) + assert.NotNil(t, err, "bad proof accepted") + } +} + +func TestGkrVectors(t *testing.T) { + + const testDirPath = "../test_vectors/" + dirEntries, err := os.ReadDir(testDirPath) + assert.NoError(t, err) + for _, dirEntry := range dirEntries { + if !dirEntry.IsDir() { + + if filepath.Ext(dirEntry.Name()) == ".json" { + path := filepath.Join(testDirPath, dirEntry.Name()) + noExt := dirEntry.Name()[:len(dirEntry.Name())-len(".json")] + + t.Run(noExt+"_prover", generateTestProver(path)) + t.Run(noExt+"_verifier", generateTestVerifier(path)) + + } + } + } +} + +func proofEquals(expected Proof, seen Proof) error { + if len(expected) != len(seen) { + return fmt.Errorf("length mismatch %d ≠ %d", len(expected), len(seen)) + } + for i, x := range expected { + xSeen := seen[i] + + if xSeen.finalEvalProof == nil { + if seenFinalEval := x.finalEvalProof; len(seenFinalEval) != 0 { + return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) + } + } else { + if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + return fmt.Errorf("final evaluation proof mismatch") + } + } + if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + return err + } + } + return nil +} + +func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { + fmt.Println("creating circuit structure") + c := mimcCircuit(mimcDepth) + + in0 := make([]fr.Element, nbInstances) + in1 := make([]fr.Element, nbInstances) + fr.Vector(in0).MustSetRandom() + fr.Vector(in1).MustSetRandom() + + fmt.Println("evaluating circuit") + start := time.Now().UnixMicro() + assignment := WireAssignment{in0, in1}.Complete(utils.References(c)) + solved := time.Now().UnixMicro() - start + fmt.Println("solved in", solved, "μs") + + //b.ResetTimer() + fmt.Println("constructing proof") + start = time.Now().UnixMicro() + _, err := Prove(c, assignment, fiatshamir.WithHash(mimc.NewMiMC())) + proved := time.Now().UnixMicro() - start + fmt.Println("proved in", proved, "μs") + assert.NoError(b, err) +} + +func BenchmarkGkrMimc19(b *testing.B) { + benchmarkGkrMiMC(b, 1<<19, 91) +} + +func BenchmarkGkrMimc17(b *testing.B) { + benchmarkGkrMiMC(b, 1<<17, 91) +} + +func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { + proof := make(Proof, len(printable)) + for i := range printable { + finalEvalProof := []fr.Element(nil) + + if printable[i].FinalEvalProof != nil { + finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) + finalEvalProof = make([]fr.Element, finalEvalSlice.Len()) + for k := range finalEvalProof { + if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + return nil, err + } + } + } + + proof[i] = sumcheckProof{ + partialSumPolys: make([]polynomial.Polynomial, len(printable[i].PartialSumPolys)), + finalEvalProof: finalEvalProof, + } + for k := range printable[i].PartialSumPolys { + var err error + if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + return nil, err + } + } + } + return proof, nil +} + +type TestCase struct { + Circuit gkrtypes.Circuit + Hash hash.Hash + Proof Proof + FullAssignment WireAssignment + InOutAssignment WireAssignment +} + +var ( + testCases = make(map[string]*TestCase) + cache = gkrtesting.NewCache() +) + +func newTestCase(path string) (*TestCase, error) { + path, err := filepath.Abs(path) + if err != nil { + return nil, err + } + dir := filepath.Dir(path) + + tCase, ok := testCases[path] + if ok { + return tCase, nil + } + + info, err := cache.ReadTestCaseInfo(path) + if err != nil { + return nil, err + } + + circuit := cache.GetCircuit(filepath.Join(dir, info.Circuit)) + var _hash hash.Hash + if _hash, err = hashFromDescription(info.Hash); err != nil { + return nil, err + } + var proof Proof + if proof, err = unmarshalProof(info.Proof); err != nil { + return nil, err + } + + fullAssignment := make(WireAssignment, len(circuit)) + inOutAssignment := make(WireAssignment, len(circuit)) + + sorted := circuit.TopologicalSort() + + inI, outI := 0, 0 + for i, w := range sorted { + var assignmentRaw []interface{} + if w.IsInput() { + if inI == len(info.Input) { + return nil, fmt.Errorf("fewer input in vector than in circuit") + } + assignmentRaw = info.Input[inI] + inI++ + } else if w.IsOutput() { + if outI == len(info.Output) { + return nil, fmt.Errorf("fewer output in vector than in circuit") + } + assignmentRaw = info.Output[outI] + outI++ + } + if assignmentRaw != nil { + var wireAssignment []fr.Element + if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + return nil, err + } + + fullAssignment[i] = wireAssignment + inOutAssignment[i] = wireAssignment + } + } + + fullAssignment.Complete(utils.References(circuit)) + + for i, w := range sorted { + if w.IsOutput() { + if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + return nil, fmt.Errorf("assignment mismatch: %v", err) + } + } + } + + tCase = &TestCase{ + FullAssignment: fullAssignment, + InOutAssignment: inOutAssignment, + Proof: proof, + Hash: _hash, + Circuit: circuit, + } + + testCases[path] = tCase + + return tCase, nil +} diff --git a/internal/gkr/bls12-381/solver_hints.go b/internal/gkr/bls12-381/solver_hints.go new file mode 100644 index 00000000..cb498c78 --- /dev/null +++ b/internal/gkr/bls12-381/solver_hints.go @@ -0,0 +1,146 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark-crypto/hash" + "github.com/consensys/gnark-crypto/utils" + hint "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + algo_utils "github.com/consensys/gnark/internal/utils" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +type SolvingData struct { + assignment WireAssignment + circuit gkrtypes.Circuit + workers *utils.WorkerPool +} + +func (d *SolvingData) init(info gkrtypes.SolvingInfo) { + d.workers = utils.NewWorkerPool() + d.circuit = info.Circuit + d.circuit.SetNbUniqueOutputs() + + d.assignment = make(WireAssignment, len(d.circuit)) + for i := range d.assignment { + d.assignment[i] = make([]fr.Element, info.NbInstances) + } +} + +// this module assumes that wire and instance indexes respect dependencies + +func setOuts(a WireAssignment, circuit gkrtypes.Circuit, outs []*big.Int) { + outsI := 0 + for i := range circuit { + if circuit[i].IsOutput() { + for j := range a[i] { + a[i][j].BigInt(outs[outsI]) + outsI++ + } + } + } + // Check if outsI == len(outs)? +} + +func SolveHint(info gkrtypes.SolvingInfo, data *SolvingData) hint.Hint { + return func(_ *big.Int, ins, outs []*big.Int) error { + // assumes assignmentVector is arranged wire first, instance second in order of solution + offsets := info.AssignmentOffsets() + data.init(info) + maxNIn := data.circuit.MaxGateNbIn() + + chunks := info.Chunks() + + solveTask := func(chunkOffset int) utils.Task { + return func(startInChunk, endInChunk int) { + start := startInChunk + chunkOffset + end := endInChunk + chunkOffset + inputs := make([]frontend.Variable, maxNIn) + dependencyHeads := make([]int, len(data.circuit)) // for each wire, which of its dependencies we would look at next + for wI := range data.circuit { // skip instances that are not relevant (related to instances before the current task) + deps := info.Dependencies[wI] + dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { + return deps[i].InputInstance + }, len(deps), start) + } + + for instanceI := start; instanceI < end; instanceI++ { + for wireI := range data.circuit { + wire := &data.circuit[wireI] + deps := info.Dependencies[wireI] + if wire.IsInput() { + if dependencyHeads[wireI] < len(deps) && instanceI == deps[dependencyHeads[wireI]].InputInstance { + dep := deps[dependencyHeads[wireI]] + data.assignment[wireI][instanceI].Set(&data.assignment[dep.OutputWire][dep.OutputInstance]) + dependencyHeads[wireI]++ + } else { + data.assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) + } + } else { + // assemble the inputs + inputIndexes := info.Circuit[wireI].Inputs + for i, inputI := range inputIndexes { + inputs[i] = &data.assignment[inputI][instanceI] + } + gate := data.circuit[wireI].Gate + data.assignment[wireI][instanceI].Set(gate.Evaluate(api, inputs[:len(inputIndexes)]...).(*fr.Element)) + } + } + } + } + } + + start := 0 + for _, end := range chunks { + data.workers.Submit(end-start, solveTask(start), 1024).Wait() + start = end + } + + for _, p := range info.Prints { + serializable := make([]any, len(p.Values)) + for i, v := range p.Values { + if p.IsGkrVar[i] { // serializer stores uint32 in slices as uint64 + serializable[i] = data.assignment[algo_utils.ForceUint32(v)][p.Instance].String() + } else { + serializable[i] = v + } + } + fmt.Println(serializable...) + } + + setOuts(data.assignment, info.Circuit, outs) + + return nil + } +} + +func ProveHint(hashName string, data *SolvingData) hint.Hint { + + return func(_ *big.Int, ins, outs []*big.Int) error { + insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called + b := make([]byte, fr.Bytes) + i.FillBytes(b) + return b[:] + }) + + hsh := hash.NewHash(hashName + "_BLS12_381") + + proof, err := Prove(data.circuit, data.assignment, fiatshamir.WithHash(hsh, insBytes...), WithWorkers(data.workers)) + if err != nil { + return err + } + + return proof.SerializeToBigInts(outs) + + } +} diff --git a/internal/gkr/bls12-381/sumcheck.go b/internal/gkr/bls12-381/sumcheck.go new file mode 100644 index 00000000..266110d3 --- /dev/null +++ b/internal/gkr/bls12-381/sumcheck.go @@ -0,0 +1,171 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "strconv" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +// This does not make use of parallelism and represents polynomials as lists of coefficients +// It is currently geared towards arithmetic hashes. Once we have a more unified hash function interface, this can be generified. + +// sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. +// Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) +type sumcheckClaims interface { + combine(a fr.Element) polynomial.Polynomial // combine into the 0ᵗʰ sumcheck subclaim. Create g := ∑_{1≤j≤m} aʲ⁻¹fⱼ for which now we seek to prove ∑_{0≤i<2ⁿ} g(i) = c := ∑_{1≤j≤m} aʲ⁻¹cⱼ. Return g₁. + next(fr.Element) polynomial.Polynomial // Return the evaluations gⱼ(k) for 1 ≤ k < degⱼ(g). Update the claim to gⱼ₊₁ for the input value as rⱼ + varsNum() int // number of variables + claimsNum() int // number of claims + proveFinalEval(r []fr.Element) []fr.Element // in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +// sumcheckLazyClaims is the sumcheckClaims data structure on the verifier side. It is "lazy" in that it has to compute fewer things. +type sumcheckLazyClaims interface { + claimsNum() int // claimsNum = m + varsNum() int // varsNum = n + combinedSum(a fr.Element) fr.Element // combinedSum returns c = ∑_{1≤j≤m} aʲ⁻¹cⱼ + degree(i int) int // degree of the total claim in the i'th variable + verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error +} + +// sumcheckProof of a multi-statement. +type sumcheckProof struct { + partialSumPolys []polynomial.Polynomial + finalEvalProof []fr.Element //in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +func setupTranscript(claimsNum int, varsNum int, settings *fiatshamir.Settings) (challengeNames []string, err error) { + numChallenges := varsNum + if claimsNum >= 2 { + numChallenges++ + } + challengeNames = make([]string, numChallenges) + if claimsNum >= 2 { + challengeNames[0] = settings.Prefix + "comb" + } + prefix := settings.Prefix + "pSP." + for i := 0; i < varsNum; i++ { + challengeNames[i+numChallenges-varsNum] = prefix + strconv.Itoa(i) + } + if settings.Transcript == nil { + transcript := fiatshamir.NewTranscript(settings.Hash, challengeNames...) + settings.Transcript = transcript + } + + for i := range settings.BaseChallenges { + if err = settings.Transcript.Bind(challengeNames[0], settings.BaseChallenges[i]); err != nil { + return + } + } + return +} + +func next(transcript *fiatshamir.Transcript, bindings []fr.Element, remainingChallengeNames *[]string) (fr.Element, error) { + challengeName := (*remainingChallengeNames)[0] + for i := range bindings { + bytes := bindings[i].Bytes() + if err := transcript.Bind(challengeName, bytes[:]); err != nil { + return fr.Element{}, err + } + } + var res fr.Element + bytes, err := transcript.ComputeChallenge(challengeName) + res.SetBytes(bytes) + + *remainingChallengeNames = (*remainingChallengeNames)[1:] + + return res, err +} + +// sumcheckProve create a non-interactive proof +func sumcheckProve(claims sumcheckClaims, transcriptSettings fiatshamir.Settings) (sumcheckProof, error) { + + var proof sumcheckProof + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return proof, err + } + + var combinationCoeff fr.Element + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return proof, err + } + } + + varsNum := claims.varsNum() + proof.partialSumPolys = make([]polynomial.Polynomial, varsNum) + proof.partialSumPolys[0] = claims.combine(combinationCoeff) + challenges := make([]fr.Element, varsNum) + + for j := 0; j+1 < varsNum; j++ { + if challenges[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return proof, err + } + proof.partialSumPolys[j+1] = claims.next(challenges[j]) + } + + if challenges[varsNum-1], err = next(transcript, proof.partialSumPolys[varsNum-1], &remainingChallengeNames); err != nil { + return proof, err + } + + proof.finalEvalProof = claims.proveFinalEval(challenges) + + return proof, nil +} + +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, transcriptSettings fiatshamir.Settings) error { + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return err + } + + var combinationCoeff fr.Element + + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return err + } + } + + r := make([]fr.Element, claims.varsNum()) + + // Just so that there is enough room for gJ to be reused + maxDegree := claims.degree(0) + for j := 1; j < claims.varsNum(); j++ { + if d := claims.degree(j); d > maxDegree { + maxDegree = d + } + } + gJ := make(polynomial.Polynomial, maxDegree+1) //At the end of iteration j, gJ = ∑_{i < 2ⁿ⁻ʲ⁻¹} g(X₁, ..., Xⱼ₊₁, i...) NOTE: n is shorthand for claims.varsNum() + gJR := claims.combinedSum(combinationCoeff) // At the beginning of iteration j, gJR = ∑_{i < 2ⁿ⁻ʲ} g(r₁, ..., rⱼ, i...) + + for j := range claims.varsNum() { + if len(proof.partialSumPolys[j]) != claims.degree(j) { + return errors.New("malformed proof") + } + copy(gJ[1:], proof.partialSumPolys[j]) + gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) // Requirement that gⱼ(0) + gⱼ(1) = gⱼ₋₁(r) + // gJ is ready + + //Prepare for the next iteration + if r[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return err + } + // This is an extremely inefficient way of interpolating. TODO: Interpolate without symbolically computing a polynomial + gJCoeffs := polynomial.InterpolateOnRange(gJ[:(claims.degree(j) + 1)]) + gJR = gJCoeffs.Eval(&r[j]) + } + + return claims.verifyFinalEval(r, combinationCoeff, gJR, proof.finalEvalProof) +} diff --git a/internal/gkr/bls12-381/sumcheck_test.go b/internal/gkr/bls12-381/sumcheck_test.go new file mode 100644 index 00000000..60abec5b --- /dev/null +++ b/internal/gkr/bls12-381/sumcheck_test.go @@ -0,0 +1,152 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/stretchr/testify/assert" + + "math/bits" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + + "strings" + "testing" +) + +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { + poly := make(polynomial.MultiLin, len(polyInt)) + for i, n := range polyInt { + poly[i].SetUint64(n) + } + + claim := singleMultilinClaim{g: poly.Clone()} + + proof, err := sumcheckProve(&claim, fiatshamir.WithHash(hashGenerator())) + if err != nil { + return err + } + + var sb strings.Builder + for _, p := range proof.partialSumPolys { + + sb.WriteString("\t{") + for i := 0; i < len(p); i++ { + sb.WriteString(p[i].String()) + if i+1 < len(p) { + sb.WriteString(", ") + } + } + sb.WriteString("}\n") + } + + lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if err = sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())); err != nil { + return err + } + + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())) == nil { + return fmt.Errorf("bad proof accepted") + } + return nil +} + +func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { + + polys := [][]uint64{ + {1, 2, 3, 4}, // 1 + 2X₁ + X₂ + {1, 2, 3, 4, 5, 6, 7, 8}, // 1 + 4X₁ + 2X₂ + X₃ + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, // 1 + 8X₁ + 4X₂ + 2X₃ + X₄ + } + + const MaxStep = 4 + const MaxStart = 4 + hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + + for step := 0; step < MaxStep; step++ { + for startState := 0; startState < MaxStart; startState++ { + if step == 0 && startState == 1 { // unlucky case where a bad proof would be accepted + continue + } + hashGens = append(hashGens, newMessageCounterGenerator(startState, step)) + } + } + + for _, poly := range polys { + for _, hashGen := range hashGens { + assert.NoError(t, testSumcheckSingleClaimMultilin(poly, hashGen), + "failed with poly %v and hashGen %v", poly, hashGen()) + } + } +} + +type singleMultilinClaim struct { + g polynomial.MultiLin +} + +func (c singleMultilinClaim) proveFinalEval(r []fr.Element) []fr.Element { + return nil // verifier can compute the final eval itself +} + +func (c singleMultilinClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} + +func (c singleMultilinClaim) claimsNum() int { + return 1 +} + +func sumForX1One(g polynomial.MultiLin) polynomial.Polynomial { + sum := g[len(g)/2] + for i := len(g)/2 + 1; i < len(g); i++ { + sum.Add(&sum, &g[i]) + } + return []fr.Element{sum} +} + +func (c singleMultilinClaim) combine(fr.Element) polynomial.Polynomial { + return sumForX1One(c.g) +} + +func (c *singleMultilinClaim) next(r fr.Element) polynomial.Polynomial { + c.g.Fold(r) + return sumForX1One(c.g) +} + +type singleMultilinLazyClaim struct { + g polynomial.MultiLin + claimedSum fr.Element +} + +func (c singleMultilinLazyClaim) verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error { + val := c.g.Evaluate(r, nil) + if val.Equal(&purportedValue) { + return nil + } + return fmt.Errorf("mismatch") +} + +func (c singleMultilinLazyClaim) combinedSum(combinationCoeffs fr.Element) fr.Element { + return c.claimedSum +} + +func (c singleMultilinLazyClaim) degree(i int) int { + return 1 +} + +func (c singleMultilinLazyClaim) claimsNum() int { + return 1 +} + +func (c singleMultilinLazyClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} diff --git a/internal/gkr/bls12-381/test_vector_utils_test.go b/internal/gkr/bls12-381/test_vector_utils_test.go new file mode 100644 index 00000000..315673fc --- /dev/null +++ b/internal/gkr/bls12-381/test_vector_utils_test.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "strings" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrtesting" +) + +func toElement(i int64) *fr.Element { + var res fr.Element + res.SetInt64(i) + return &res +} + +func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { + if _type, ok := d["type"]; ok { + switch _type { + case "const": + startState := int64(d["val"].(float64)) + return &messageCounter{startState: startState, step: 0, state: startState}, nil + default: + return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) + } + } + return nil, fmt.Errorf("hash description missing type") +} + +type messageCounter struct { + startState int64 + state int64 + step int64 +} + +func (m *messageCounter) Write(p []byte) (n int, err error) { + inputBlockSize := (len(p)-1)/fr.Bytes + 1 + m.state += int64(inputBlockSize) * m.step + return len(p), nil +} + +func (m *messageCounter) Sum(b []byte) []byte { + inputBlockSize := (len(b)-1)/fr.Bytes + 1 + resI := m.state + int64(inputBlockSize)*m.step + var res fr.Element + res.SetInt64(int64(resI)) + resBytes := res.Bytes() + return resBytes[:] +} + +func (m *messageCounter) Reset() { + m.state = m.startState +} + +func (m *messageCounter) Size() int { + return fr.Bytes +} + +func (m *messageCounter) BlockSize() int { + return fr.Bytes +} + +func newMessageCounter(startState, step int) hash.Hash { + transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} + return transcript +} + +func newMessageCounterGenerator(startState, step int) func() hash.Hash { + return func() hash.Hash { + return newMessageCounter(startState, step) + } +} + +func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { + + // TODO: Put this in element.SetString? + switch v := value.(type) { + case string: + + if sep := strings.Split(v, "/"); len(sep) == 2 { + var denom fr.Element + if _, err := z.SetString(sep[0]); err != nil { + return nil, err + } + if _, err := denom.SetString(sep[1]); err != nil { + return nil, err + } + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil + } + + case float64: + asInt := int64(v) + if float64(asInt) != v { + return nil, fmt.Errorf("cannot currently parse float") + } + z.SetInt64(asInt) + return z, nil + } + + return z.SetInterface(value) +} + +func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { + elementSlice := make([]fr.Element, len(slice)) + for i, v := range slice { + if _, err := setElement(&elementSlice[i], v); err != nil { + return nil, err + } + } + return elementSlice, nil +} + +func sliceEquals(a []fr.Element, b []fr.Element) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if !a[i].Equal(&b[i]) { + return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) + } + } + return nil +} + +func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if err := sliceEquals(a[i], b[i]); err != nil { + return fmt.Errorf("at index %d: %w", i, err) + } + } + return nil +} diff --git a/internal/gkr/bls24-315/gate_testing.go b/internal/gkr/bls24-315/gate_testing.go new file mode 100644 index 00000000..1682d247 --- /dev/null +++ b/internal/gkr/bls24-315/gate_testing.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + + "slices" + + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/polynomial" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// IsGateFunctionAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func IsGateFunctionAdditive(f gkr.GateFunction, i, nbIn int) bool { + fWrapped := api.convertFunc(f) + + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 := fWrapped(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := fWrapped(in...) + + y2.Sub(y2, y1) + y1.Sub(y1, y0) + + if !y2.Equal(y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 = fWrapped(in...) + + y1.Sub(y1, y0) + + return y1.Equal(y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f gateFunctionFr) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i].Set(f(fIn...)) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +// FindGateFunctionDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func FindGateFunctionDegree(f gkr.GateFunction, max, nbIn int) (int, error) { + fFr := api.convertFunc(f) + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := fFr.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, gkrtypes.ErrZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func VerifyGateFunctionDegree(f gkr.GateFunction, claimedDegree, nbIn int) error { + fFr := api.convertFunc(f) + if p := fFr.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return gkrtypes.ErrZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} diff --git a/internal/gkr/bls24-315/gkr.go b/internal/gkr/bls24-315/gkr.go new file mode 100644 index 00000000..7d89baf7 --- /dev/null +++ b/internal/gkr/bls24-315/gkr.go @@ -0,0 +1,816 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "sync" + + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// The goal is to prove/verify evaluations of many instances of the same circuit + +// WireAssignment is assignment of values to the same wire across many instances of the circuit +type WireAssignment []polynomial.MultiLin + +type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) + +// eqTimesGateEvalSumcheckLazyClaims is a lazy claim for sumcheck (verifier side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the checking of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckLazyClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ), allegedly + manager *claimsManager // WARNING: Circular references +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) getWire() *gkrtypes.Wire { + return e.manager.wires[e.wireI] +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) claimsNum() int { + return len(e.evaluationPoints) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) varsNum() int { + return len(e.evaluationPoints[0]) +} + +// combinedSum returns ∑ᵢ aⁱ yᵢ +func (e *eqTimesGateEvalSumcheckLazyClaims) combinedSum(a fr.Element) fr.Element { + evalsAsPoly := polynomial.Polynomial(e.claimedEvaluations) + return evalsAsPoly.Eval(&a) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) degree(int) int { + return 1 + e.manager.wires[e.wireI].Gate.Degree() +} + +// verifyFinalEval finalizes the verification of w. +// The prover's claims w(xᵢ) = yᵢ have already been reduced to verifying +// ∑ cⁱ eq(xᵢ, r) w(r) = purportedValue. ( c is combinationCoeff ) +// Both purportedValue and the vector r have been randomized during the sumcheck protocol. +// By taking the w term out of the sum we get the equivalent claim that +// for E := ∑ eq(xᵢ, r), it must be that E w(r) = purportedValue. +// If w is an input wire, the verifier can directly check its evaluation at r. +// Otherwise, the prover makes claims about the evaluation of w's input wires, +// wᵢ, at r, to be verified later. +// The claims are communicated through the proof parameter. +// The verifier checks here if the claimed evaluations of wᵢ(r) are consistent with +// the main claim, by checking E w(wᵢ(r)...) = purportedValue. +func (e *eqTimesGateEvalSumcheckLazyClaims) verifyFinalEval(r []fr.Element, combinationCoeff, purportedValue fr.Element, uniqueInputEvaluations []fr.Element) error { + // the eq terms ( E ) + numClaims := len(e.evaluationPoints) + evaluation := polynomial.EvalEq(e.evaluationPoints[numClaims-1], r) + for i := numClaims - 2; i >= 0; i-- { + evaluation.Mul(&evaluation, &combinationCoeff) + eq := polynomial.EvalEq(e.evaluationPoints[i], r) + evaluation.Add(&evaluation, &eq) + } + + wire := e.manager.wires[e.wireI] + + // the w(...) term + var gateEvaluation fr.Element + if wire.IsInput() { // just compute w(r) + gateEvaluation = e.manager.assignment[e.wireI].Evaluate(r, e.manager.memPool) + } else { // proof contains the evaluations of the inputs, but avoids repetition in case multiple inputs come from the same wire + injection, injectionLeftInv := + e.manager.wires.ClaimPropagationInfo(e.wireI) + + if len(injection) != len(uniqueInputEvaluations) { + return fmt.Errorf("%d input wire evaluations given, %d expected", len(uniqueInputEvaluations), len(injection)) + } + + for uniqueI, i := range injection { // map from unique to all + e.manager.add(wire.Inputs[i], r, uniqueInputEvaluations[uniqueI]) + } + + inputEvaluations := make([]frontend.Variable, len(wire.Inputs)) + for i, uniqueI := range injectionLeftInv { // map from all to unique + inputEvaluations[i] = &uniqueInputEvaluations[uniqueI] + } + + gateEvaluation.Set(wire.Gate.Evaluate(api, inputEvaluations...).(*fr.Element)) + } + + evaluation.Mul(&evaluation, &gateEvaluation) + + if evaluation.Equal(&purportedValue) { + return nil + } + return errors.New("incompatible evaluations") +} + +// eqTimesGateEvalSumcheckClaims is a claim for sumcheck (prover side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the proving of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ) + manager *claimsManager + + input []polynomial.MultiLin // input[i](h₁, ..., hₘ₋ⱼ) = wᵢ(r₁, r₂, ..., rⱼ₋₁, h₁, ..., hₘ₋ⱼ) + + eq polynomial.MultiLin // E := ∑ᵢ cⁱ eq(xᵢ, -) +} + +func (c *eqTimesGateEvalSumcheckClaims) getWire() *gkrtypes.Wire { + return c.manager.wires[c.wireI] +} + +// combine the multiple claims into one claim using a random combination (combinationCoeff or c). +// From the original multiple claims of w(xᵢ) = yᵢ, we get a single claim +// ∑ᵢ,ₕ cⁱ eq(xᵢ, h) w(h) = ∑ᵢ cⁱ yᵢ, where h iterates over the hypercube (circuit instances) and +// i iterates over the claims. +// Equivalently, we could say ∑ᵢ cⁱ yᵢ = ∑ₕ,ᵢ cⁱ eq(xᵢ, h) w(h) = ∑ₕ w(h) ∑ᵢ cⁱ eq(xᵢ, h). +// Thus if we initially compute E := ∑ᵢ cⁱ eq(xᵢ, -), our claim will find the simpler form +// ∑ᵢ cⁱ yᵢ = ∑ₕ w(h) E(h), where the sum-checked polynomial is of degree deg(g) + 1, +// and deg(g) is the total degree of the polynomial defining the gate g of which w is the output. +// The output of combine is the first sumcheck claim, i.e. ∑₍ₕ₁,ₕ₂,...₎ w(X, h₁, h₂, ...) E(X, h₁, h₂, ...).. +func (c *eqTimesGateEvalSumcheckClaims) combine(combinationCoeff fr.Element) polynomial.Polynomial { + varsNum := c.varsNum() + eqLength := 1 << varsNum + claimsNum := c.claimsNum() + // initialize the eq tables ( E ) + c.eq = c.manager.memPool.Make(eqLength) + + c.eq[0].SetOne() + c.eq.Eq(c.evaluationPoints[0]) + + // E := eq(x₀, -) + newEq := polynomial.MultiLin(c.manager.memPool.Make(eqLength)) + aI := combinationCoeff + + // E += cⁱ eq(xᵢ, -) + for k := 1; k < claimsNum; k++ { + newEq[0].Set(&aI) + + c.eqAcc(c.eq, newEq, c.evaluationPoints[k]) + + if k+1 < claimsNum { + aI.Mul(&aI, &combinationCoeff) + } + } + + c.manager.memPool.Dump(newEq) + + return c.computeGJ() +} + +// eqAcc sets m to an eq table at q and then adds it to e. +// m <- eq(q, -). +// e <- e + m +func (c *eqTimesGateEvalSumcheckClaims) eqAcc(e, m polynomial.MultiLin, q []fr.Element) { + n := len(q) + + //At the end of each iteration, m(h₁, ..., hₙ) = eq(q₁, ..., qᵢ₊₁, h₁, ..., hᵢ₊₁) + for i := range q { // In the comments we use a 1-based index so q[i] = qᵢ₊₁ + // go through all assignments of (b₁, ..., bᵢ) ∈ {0,1}ⁱ + const threshold = 1 << 6 + k := 1 << i + if k < threshold { + for j := 0; j < k; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + } else { + c.manager.workers.Submit(k, func(start, end int) { + for j := start; j < end; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + }, 1024).Wait() + } + + } + c.manager.workers.Submit(len(e), func(start, end int) { + for i := start; i < end; i++ { + e[i].Add(&e[i], &m[i]) + } + }, 512).Wait() +} + +// computeGJ: gⱼ = ∑_{0≤h<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, h...) = ∑_{0≤i<2ⁿ⁻ʲ} E(r₁, ..., Xⱼ, h...) g( w₀(r₁, ..., Xⱼ, h...), ... ). +// the polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). +// The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). By convention, g₀ is a constant polynomial equal to the claimed sum. +func (c *eqTimesGateEvalSumcheckClaims) computeGJ() polynomial.Polynomial { + + wire := c.getWire() + degGJ := 1 + wire.Gate.Degree() // guaranteed to be no smaller than the actual deg(gⱼ) + nbGateIn := len(c.input) + + // Both E and wᵢ (the input wires and the eq table) are multilinear, thus + // they are linear in Xⱼ. + // So for f ∈ { E(r₁, ..., Xⱼ, h...) } ∪ {wᵢ(r₁, ..., Xⱼ, h...) }, so f(m) = m×(f(1) - f(0)) + f(0), and f(0), f(1) are easily computed from the bookkeeping tables. + // ml are such multilinear polynomials the evaluations of which over different values of Xⱼ are computed in this stepwise manner. + ml := make([]polynomial.MultiLin, nbGateIn+1) // shortcut to the evaluations of the multilinear polynomials over the hypercube + ml[0] = c.eq + copy(ml[1:], c.input) + + sumSize := len(c.eq) / 2 // the range of h, over which we sum + + // Perf-TODO: Collate once at claim "combination" time and not again. then, even folding can be done in one operation every time "next" is called + + gJ := make([]fr.Element, degGJ) + var mu sync.Mutex + computeAll := func(start, end int) { // compute method to allow parallelization across instances + var step fr.Element + + res := make([]fr.Element, degGJ) + + // evaluations of ml, laid out as: + // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), + // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), + // ... + // ml[0](degGJ, h...), ml[2](degGJ, h...), ..., ml[len(ml)-1](degGJ, h...) + mlEvals := make([]fr.Element, degGJ*len(ml)) + gateInput := make([]frontend.Variable, nbGateIn) + + for h := start; h < end; h++ { // h counts across instances + + evalAt1Index := sumSize + h + for k := range ml { + // d = 0 + mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1. Can be taken directly from the table. + step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for d := 1; d < degGJ; d++ { + mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + } + } + + eIndex := 0 // index for where the current eq term is + nextEIndex := len(ml) + for d := range degGJ { + for i := range gateInput { + gateInput[i] = &mlEvals[eIndex+1+i] + } + summand := wire.Gate.Evaluate(api, gateInput...).(*fr.Element) + summand.Mul(summand, &mlEvals[eIndex]) + res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + } + } + mu.Lock() + for i := range gJ { + gJ[i].Add(&gJ[i], &res[i]) // collect into the complete sum + } + mu.Unlock() + } + + const minBlockSize = 64 + + if sumSize < minBlockSize { + // no parallelization + computeAll(0, sumSize) + } else { + c.manager.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } + + return gJ +} + +// next first folds the input and E polynomials at the given verifier challenge then computes the new gⱼ. +// Thus, j <- j+1 and rⱼ = challenge. +func (c *eqTimesGateEvalSumcheckClaims) next(challenge fr.Element) polynomial.Polynomial { + const minBlockSize = 512 + n := len(c.eq) / 2 + if n < minBlockSize { + // no parallelization + for i := 0; i < len(c.input); i++ { + c.input[i].Fold(challenge) + } + c.eq.Fold(challenge) + } else { + wgs := make([]*sync.WaitGroup, len(c.input)) + for i := 0; i < len(c.input); i++ { + wgs[i] = c.manager.workers.Submit(n, c.input[i].FoldParallel(challenge), minBlockSize) + } + c.manager.workers.Submit(n, c.eq.FoldParallel(challenge), minBlockSize).Wait() + for _, wg := range wgs { + wg.Wait() + } + } + + return c.computeGJ() +} + +func (c *eqTimesGateEvalSumcheckClaims) varsNum() int { + return len(c.evaluationPoints[0]) +} + +func (c *eqTimesGateEvalSumcheckClaims) claimsNum() int { + return len(c.claimedEvaluations) +} + +// proveFinalEval provides the values wᵢ(r₁, ..., rₙ) +func (c *eqTimesGateEvalSumcheckClaims) proveFinalEval(r []fr.Element) []fr.Element { + //defer the proof, return list of claims + + injection, _ := c.manager.wires.ClaimPropagationInfo(c.wireI) // TODO @Tabaie: Instead of doing this last, we could just have fewer input in the first place; not that likely to happen with single gates, but more so with layers. + evaluations := make([]fr.Element, len(injection)) + for i, gateInputI := range injection { + wI := c.input[gateInputI] + wI.Fold(r[len(r)-1]) // We already have wᵢ(r₁, ..., rₙ₋₁, hₙ) in a table. Only one more fold required. + c.manager.add(c.getWire().Inputs[gateInputI], r, wI[0]) + evaluations[i] = wI[0] + } + + c.manager.memPool.Dump(c.claimedEvaluations, c.eq) + + return evaluations +} + +type claimsManager struct { + claims []*eqTimesGateEvalSumcheckLazyClaims + assignment WireAssignment + memPool *polynomial.Pool + workers *utils.WorkerPool + wires gkrtypes.Wires +} + +func newClaimsManager(wires []*gkrtypes.Wire, assignment WireAssignment, o settings) (manager claimsManager) { + manager.assignment = assignment + manager.claims = make([]*eqTimesGateEvalSumcheckLazyClaims, len(wires)) + manager.memPool = o.pool + manager.workers = o.workers + manager.wires = wires + + for i, wire := range wires { + + manager.claims[i] = &eqTimesGateEvalSumcheckLazyClaims{ + wireI: i, + evaluationPoints: make([][]fr.Element, 0, wire.NbClaims()), + claimedEvaluations: manager.memPool.Make(wire.NbClaims()), + manager: &manager, + } + } + return +} + +func (m *claimsManager) add(wire int, evaluationPoint []fr.Element, evaluation fr.Element) { + claim := m.claims[wire] + i := len(claim.evaluationPoints) + claim.claimedEvaluations[i] = evaluation + claim.evaluationPoints = append(claim.evaluationPoints, evaluationPoint) +} + +func (m *claimsManager) getLazyClaim(wire int) *eqTimesGateEvalSumcheckLazyClaims { + return m.claims[wire] +} + +func (m *claimsManager) getClaim(wireI int) *eqTimesGateEvalSumcheckClaims { + lazy := m.claims[wireI] + wire := m.wires[wireI] + res := &eqTimesGateEvalSumcheckClaims{ + wireI: wireI, + evaluationPoints: lazy.evaluationPoints, + claimedEvaluations: lazy.claimedEvaluations, + manager: m, + } + + if wire.IsInput() { + res.input = []polynomial.MultiLin{m.memPool.Clone(m.assignment[wireI])} + } else { + res.input = make([]polynomial.MultiLin, len(wire.Inputs)) + + for inputI, inputW := range wire.Inputs { + res.input[inputI] = m.memPool.Clone(m.assignment[inputW]) //will be edited later, so must be deep copied + } + } + return res +} + +func (m *claimsManager) deleteClaim(wire int) { + m.claims[wire].manager = nil + m.claims[wire] = nil +} + +type settings struct { + pool *polynomial.Pool + sorted []*gkrtypes.Wire + transcript *fiatshamir.Transcript + transcriptPrefix string + nbVars int + workers *utils.WorkerPool +} + +type Option func(*settings) + +func WithPool(pool *polynomial.Pool) Option { + return func(options *settings) { + options.pool = pool + } +} + +func WithSortedCircuit(sorted []*gkrtypes.Wire) Option { + return func(options *settings) { + options.sorted = sorted + } +} + +func WithWorkers(workers *utils.WorkerPool) Option { + return func(options *settings) { + options.workers = workers + } +} + +func setup(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (settings, error) { + var o settings + var err error + for _, option := range options { + option(&o) + } + + o.nbVars = assignment.NumVars() + nbInstances := assignment.NumInstances() + if 1< 1 { //combine the claims + size++ + } + size += logNbInstances // full run of sumcheck on logNbInstances variables + } + + nums := make([]string, max(len(sorted), logNbInstances)) + for i := range nums { + nums[i] = strconv.Itoa(i) + } + + challenges := make([]string, size) + + // output wire claims + firstChallengePrefix := prefix + "fC." + for j := 0; j < logNbInstances; j++ { + challenges[j] = firstChallengePrefix + nums[j] + } + j := logNbInstances + for i := len(sorted) - 1; i >= 0; i-- { + if sorted[i].NoProof() { + continue + } + wirePrefix := prefix + "w" + nums[i] + "." + + if sorted[i].NbClaims() > 1 { + challenges[j] = wirePrefix + "comb" + j++ + } + + partialSumPrefix := wirePrefix + "pSP." + for k := 0; k < logNbInstances; k++ { + challenges[j] = partialSumPrefix + nums[k] + j++ + } + } + return challenges +} + +func getFirstChallengeNames(logNbInstances int, prefix string) []string { + res := make([]string, logNbInstances) + firstChallengePrefix := prefix + "fC." + for i := 0; i < logNbInstances; i++ { + res[i] = firstChallengePrefix + strconv.Itoa(i) + } + return res +} + +func getChallenges(transcript *fiatshamir.Transcript, names []string) ([]fr.Element, error) { + res := make([]fr.Element, len(names)) + for i, name := range names { + if bytes, err := transcript.ComputeChallenge(name); err == nil { + res[i].SetBytes(bytes) + } else { + return nil, err + } + } + return res, nil +} + +// Prove consistency of the claimed assignment +func Prove(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (Proof, error) { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return nil, err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + proof := make(Proof, len(c)) + // firstChallenge called rho in the paper + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return nil, err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + claim := claims.getClaim(i) + if wire.NoProof() { // input wires with one claim only + proof[i] = sumcheckProof{ + partialSumPolys: []polynomial.Polynomial{}, + finalEvalProof: []fr.Element{}, + } + } else { + if proof[i], err = sumcheckProve( + claim, fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err != nil { + return proof, err + } + + baseChallenge = make([][]byte, len(proof[i].finalEvalProof)) + for j := range proof[i].finalEvalProof { + baseChallenge[j] = proof[i].finalEvalProof[j].Marshal() + } + } + // the verifier checks a single claim about input wires itself + claims.deleteClaim(i) + } + + return proof, nil +} + +// Verify the consistency of the claimed output with the claimed input +// Unlike in Prove, the assignment argument need not be complete +func Verify(c gkrtypes.Circuit, assignment WireAssignment, proof Proof, transcriptSettings fiatshamir.Settings, options ...Option) error { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + proofW := proof[i] + claim := claims.getLazyClaim(i) + if wire.NoProof() { // input wires with one claim only + // make sure the proof is empty + if len(proofW.finalEvalProof) != 0 || len(proofW.partialSumPolys) != 0 { + return errors.New("no proof allowed for input wire with a single claim") + } + + if wire.NbClaims() == 1 { // input wire + // simply evaluate and see if it matches + evaluation := assignment[i].Evaluate(claim.evaluationPoints[0], claims.memPool) + if !claim.claimedEvaluations[0].Equal(&evaluation) { + return errors.New("incorrect input wire claim") + } + } + } else if err = sumcheckVerify( + claim, proof[i], fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err == nil { // incorporate prover claims about w's input into the transcript + baseChallenge = make([][]byte, len(proofW.finalEvalProof)) + for j := range baseChallenge { + baseChallenge[j] = proofW.finalEvalProof[j].Marshal() + } + } else { + return fmt.Errorf("sumcheck proof rejected: %v", err) //TODO: Any polynomials to dump? + } + claims.deleteClaim(i) + } + return nil +} + +// Complete the circuit evaluation from input values +func (a WireAssignment) Complete(wires gkrtypes.Wires) WireAssignment { + + nbInstances := a.NumInstances() + maxNbIns := 0 + + for i, w := range wires { + maxNbIns = max(maxNbIns, len(w.Inputs)) + if len(a[i]) != nbInstances { + a[i] = make([]fr.Element, nbInstances) + } + } + + ins := make([]fr.Element, maxNbIns) + for i := range nbInstances { + for wI, w := range wires { + if !w.IsInput() { + for inI, in := range w.Inputs { + ins[inI] = a[in][i] + } + a[wI][i].Set(api.evaluate(w.Gate.Evaluate, ins[:len(w.Inputs)]...)) + } + } + } + + return a +} + +func (a WireAssignment) NumInstances() int { + for _, aW := range a { + return len(aW) + } + panic("empty assignment") +} + +func (a WireAssignment) NumVars() int { + for _, aW := range a { + return aW.NumVars() + } + panic("empty assignment") +} + +// SerializeToBigInts flattens a proof object into the given slice of big.Ints +// useful in gnark hints. +func (p Proof) SerializeToBigInts(outs []*big.Int) error { + offset := 0 + for i := range p { + for _, poly := range p[i].partialSumPolys { + frToBigInts(outs[offset:], poly) + offset += len(poly) + } + if p[i].finalEvalProof != nil { + frToBigInts(outs[offset:], p[i].finalEvalProof) + offset += len(p[i].finalEvalProof) + } + } + if offset != len(outs) { + return fmt.Errorf("expected %d elements, got %d", offset, len(outs)) + } + return nil +} + +func frToBigInts(dst []*big.Int, src []fr.Element) { + for i := range src { + src[i].BigInt(dst[i]) + } +} + +// gateAPI implements gkr.GateAPI. +type gateAPI struct{} + +var api gateAPI + +func (gateAPI) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element // TODO Heap allocated. Keep an eye on perf + res.Add(cast(i1), cast(i2)) + for _, v := range in { + res.Add(&res, cast(v)) + } + return &res +} + +func (gateAPI) MulAcc(a, b, c frontend.Variable) frontend.Variable { + var prod fr.Element + prod.Add(cast(b), cast(c)) + res := cast(a) + res.Add(res, &prod) + return &res +} + +func (gateAPI) Neg(i1 frontend.Variable) frontend.Variable { + var res fr.Element + res.Neg(cast(i1)) + return &res +} + +func (gateAPI) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Sub(cast(i1), cast(i2)) + for _, v := range in { + res.Sub(&res, cast(v)) + } + return &res +} + +func (gateAPI) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Mul(cast(i1), cast(i2)) + for _, v := range in { + res.Mul(&res, cast(v)) + } + return &res +} + +func (gateAPI) Println(a ...frontend.Variable) { + toPrint := make([]any, len(a)) + var x fr.Element + + for i, v := range a { + if _, err := x.SetInterface(v); err != nil { + toPrint[i] = x.String() + } else { + if s, ok := v.(string); ok { + toPrint[i] = s + continue + } + panic(fmt.Errorf("not numeric or string: %w", err)) + } + } + fmt.Println(toPrint...) +} + +func (api gateAPI) evaluate(f gkr.GateFunction, in ...fr.Element) *fr.Element { + inVar := make([]frontend.Variable, len(in)) + for i := range in { + inVar[i] = &in[i] + } + return f(api, inVar...).(*fr.Element) +} + +type gateFunctionFr func(...fr.Element) *fr.Element + +// convertFunc turns f into a function that accepts and returns fr.Element. +func (api gateAPI) convertFunc(f gkr.GateFunction) gateFunctionFr { + return func(in ...fr.Element) *fr.Element { + return api.evaluate(f, in...) + } +} + +func cast(v frontend.Variable) *fr.Element { + if x, ok := v.(*fr.Element); ok { // fast path, no extra heap allocation + return x + } + var x fr.Element + if _, err := x.SetInterface(v); err != nil { + panic(err) + } + return &x +} diff --git a/internal/gkr/bls24-315/gkr_test.go b/internal/gkr/bls24-315/gkr_test.go new file mode 100644 index 00000000..ee94fe9d --- /dev/null +++ b/internal/gkr/bls24-315/gkr_test.go @@ -0,0 +1,505 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "os" + "path/filepath" + "reflect" + "strconv" + "testing" + "time" + + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/mimc" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + gcUtils "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/internal/utils" + "github.com/consensys/gnark/std/gkrapi/gkr" + "github.com/stretchr/testify/assert" +) + +func TestNoGateTwoInstances(t *testing.T) { + // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case + testNoGate(t, []fr.Element{four, three}) +} + +func TestNoGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}}) +} + +func TestSingleAddGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Add2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleMulGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Mul2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleInputTwoIdentityGates(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + }) +} + +func TestSingleInputTwoIdentityGatesComposed(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{1}, + }}) +} + +func TestAPowNTimesBCircuit(t *testing.T) { + const N = 10 + + c := make(gkrtypes.Circuit, N+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: gkrtypes.Mul2(), + Inputs: []int{i - 1, 0}, + } + } + + test(t, c) +} + +func TestSingleMimcCipherGate(t *testing.T) { + test(t, gkrtypes.Circuit{ + {}, {}, + { + Inputs: []int{0, 1}, + Gate: cache.GetGate("mimc"), + }, + }) +} + +func TestShallowMimcTwoInstances(t *testing.T) { + test(t, mimcCircuit(2)) +} + +func TestMimc(t *testing.T) { + test(t, mimcCircuit(93)) +} + +func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { + circuit := gkrtypes.Circuit{gkrtypes.Wire{ + Gate: gkrtypes.Identity(), + NbUniqueOutputs: 2, + }} + + assignment := WireAssignment{[]fr.Element{two, three}} + var o settings + pool := polynomial.NewPool(256, 1<<11) + workers := gcUtils.NewWorkerPool() + o.pool = &pool + o.workers = workers + + claimsManagerGen := func() *claimsManager { + manager := newClaimsManager(utils.References(circuit), assignment, o) + manager.add(0, []fr.Element{three}, five) + manager.add(0, []fr.Element{four}, six) + return &manager + } + + transcriptGen := newMessageCounterGenerator(4, 1) + + proof, err := sumcheckProve(claimsManagerGen().getClaim(0), fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) + err = sumcheckVerify(claimsManagerGen().getLazyClaim(0), proof, fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) +} + +var one, two, three, four, five, six fr.Element + +func init() { + one.SetOne() + two.Double(&one) + three.Add(&two, &one) + four.Double(&two) + five.Add(&three, &two) + six.Double(&three) +} + +var testManyInstancesLogMaxInstances = -1 + +func getLogMaxInstances(t *testing.T) int { + if testManyInstancesLogMaxInstances == -1 { + + s := os.Getenv("GKR_LOG_INSTANCES") + if s == "" { + testManyInstancesLogMaxInstances = 5 + } else { + var err error + testManyInstancesLogMaxInstances, err = strconv.Atoi(s) + if err != nil { + t.Error(err) + } + } + + } + return testManyInstancesLogMaxInstances +} + +func test(t *testing.T, circuit gkrtypes.Circuit) { + wireRefs := utils.References(circuit) + ins := circuit.Inputs() + insAssignment := make(WireAssignment, len(ins)) + maxSize := 1 << getLogMaxInstances(t) + + for i := range ins { + insAssignment[i] = make([]fr.Element, maxSize) + fr.Vector(insAssignment[i]).MustSetRandom() + } + + fullAssignment := make(WireAssignment, len(circuit)) + for _, numEvals := range []int{2, maxSize} { + for i := range ins { + fullAssignment[ins[i]] = insAssignment[i][:numEvals] + } + + fullAssignment.Complete(wireRefs) + + t.Log("Selected inputs for test") + + proof, err := Prove(circuit, fullAssignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") + + if proof.isEmpty() { // special case for TestNoGate: + continue // there's no way to make a trivial proof fail + } + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(0, 1))) + assert.NotNil(t, err, "bad proof accepted") + } + +} + +func (p Proof) isEmpty() bool { + for i := range p { + if len(p[i].finalEvalProof) != 0 { + return false + } + for j := range p[i].partialSumPolys { + if len(p[i].partialSumPolys[j]) != 0 { + return false + } + } + } + return true +} + +func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { + c := gkrtypes.Circuit{ + {}, + } + + assignment := WireAssignment{0: inputAssignments[0]} + + proof, err := Prove(c, assignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(c, assignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") +} + +func mimcCircuit(numRounds int) gkrtypes.Circuit { + c := make(gkrtypes.Circuit, numRounds+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: cache.GetGate("mimc"), + Inputs: []int{i - 1, 0}, + } + } + return c +} + +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("bivariate input needed") + } + res := api.Add(x[0], x[1]) + return api.Mul(res, x[0]) + } + + // g: x,y -> x² + 3y + g := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + res := api.Mul(x[0], x[0]) + y3 := api.Mul(x[1], 3) + return api.Add(res, y3) + } + + // h: x -> 2x + // but it edits it input + h := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + return api.Add(x[0], x[0]) + } + + assert.False(t, IsGateFunctionAdditive(f, 1, 2)) + assert.False(t, IsGateFunctionAdditive(f, 0, 2)) + + assert.False(t, IsGateFunctionAdditive(g, 0, 2)) + assert.True(t, IsGateFunctionAdditive(g, 1, 2)) + + assert.True(t, IsGateFunctionAdditive(h, 0, 1)) +} + +func generateTestProver(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + proof, err := Prove(testCase.Circuit, testCase.FullAssignment, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err) + assert.NoError(t, proofEquals(testCase.Proof, proof)) + } +} + +func generateTestVerifier(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err, "proof rejected") + testCase, err = newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(newMessageCounter(2, 0))) + assert.NotNil(t, err, "bad proof accepted") + } +} + +func TestGkrVectors(t *testing.T) { + + const testDirPath = "../test_vectors/" + dirEntries, err := os.ReadDir(testDirPath) + assert.NoError(t, err) + for _, dirEntry := range dirEntries { + if !dirEntry.IsDir() { + + if filepath.Ext(dirEntry.Name()) == ".json" { + path := filepath.Join(testDirPath, dirEntry.Name()) + noExt := dirEntry.Name()[:len(dirEntry.Name())-len(".json")] + + t.Run(noExt+"_prover", generateTestProver(path)) + t.Run(noExt+"_verifier", generateTestVerifier(path)) + + } + } + } +} + +func proofEquals(expected Proof, seen Proof) error { + if len(expected) != len(seen) { + return fmt.Errorf("length mismatch %d ≠ %d", len(expected), len(seen)) + } + for i, x := range expected { + xSeen := seen[i] + + if xSeen.finalEvalProof == nil { + if seenFinalEval := x.finalEvalProof; len(seenFinalEval) != 0 { + return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) + } + } else { + if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + return fmt.Errorf("final evaluation proof mismatch") + } + } + if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + return err + } + } + return nil +} + +func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { + fmt.Println("creating circuit structure") + c := mimcCircuit(mimcDepth) + + in0 := make([]fr.Element, nbInstances) + in1 := make([]fr.Element, nbInstances) + fr.Vector(in0).MustSetRandom() + fr.Vector(in1).MustSetRandom() + + fmt.Println("evaluating circuit") + start := time.Now().UnixMicro() + assignment := WireAssignment{in0, in1}.Complete(utils.References(c)) + solved := time.Now().UnixMicro() - start + fmt.Println("solved in", solved, "μs") + + //b.ResetTimer() + fmt.Println("constructing proof") + start = time.Now().UnixMicro() + _, err := Prove(c, assignment, fiatshamir.WithHash(mimc.NewMiMC())) + proved := time.Now().UnixMicro() - start + fmt.Println("proved in", proved, "μs") + assert.NoError(b, err) +} + +func BenchmarkGkrMimc19(b *testing.B) { + benchmarkGkrMiMC(b, 1<<19, 91) +} + +func BenchmarkGkrMimc17(b *testing.B) { + benchmarkGkrMiMC(b, 1<<17, 91) +} + +func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { + proof := make(Proof, len(printable)) + for i := range printable { + finalEvalProof := []fr.Element(nil) + + if printable[i].FinalEvalProof != nil { + finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) + finalEvalProof = make([]fr.Element, finalEvalSlice.Len()) + for k := range finalEvalProof { + if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + return nil, err + } + } + } + + proof[i] = sumcheckProof{ + partialSumPolys: make([]polynomial.Polynomial, len(printable[i].PartialSumPolys)), + finalEvalProof: finalEvalProof, + } + for k := range printable[i].PartialSumPolys { + var err error + if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + return nil, err + } + } + } + return proof, nil +} + +type TestCase struct { + Circuit gkrtypes.Circuit + Hash hash.Hash + Proof Proof + FullAssignment WireAssignment + InOutAssignment WireAssignment +} + +var ( + testCases = make(map[string]*TestCase) + cache = gkrtesting.NewCache() +) + +func newTestCase(path string) (*TestCase, error) { + path, err := filepath.Abs(path) + if err != nil { + return nil, err + } + dir := filepath.Dir(path) + + tCase, ok := testCases[path] + if ok { + return tCase, nil + } + + info, err := cache.ReadTestCaseInfo(path) + if err != nil { + return nil, err + } + + circuit := cache.GetCircuit(filepath.Join(dir, info.Circuit)) + var _hash hash.Hash + if _hash, err = hashFromDescription(info.Hash); err != nil { + return nil, err + } + var proof Proof + if proof, err = unmarshalProof(info.Proof); err != nil { + return nil, err + } + + fullAssignment := make(WireAssignment, len(circuit)) + inOutAssignment := make(WireAssignment, len(circuit)) + + sorted := circuit.TopologicalSort() + + inI, outI := 0, 0 + for i, w := range sorted { + var assignmentRaw []interface{} + if w.IsInput() { + if inI == len(info.Input) { + return nil, fmt.Errorf("fewer input in vector than in circuit") + } + assignmentRaw = info.Input[inI] + inI++ + } else if w.IsOutput() { + if outI == len(info.Output) { + return nil, fmt.Errorf("fewer output in vector than in circuit") + } + assignmentRaw = info.Output[outI] + outI++ + } + if assignmentRaw != nil { + var wireAssignment []fr.Element + if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + return nil, err + } + + fullAssignment[i] = wireAssignment + inOutAssignment[i] = wireAssignment + } + } + + fullAssignment.Complete(utils.References(circuit)) + + for i, w := range sorted { + if w.IsOutput() { + if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + return nil, fmt.Errorf("assignment mismatch: %v", err) + } + } + } + + tCase = &TestCase{ + FullAssignment: fullAssignment, + InOutAssignment: inOutAssignment, + Proof: proof, + Hash: _hash, + Circuit: circuit, + } + + testCases[path] = tCase + + return tCase, nil +} diff --git a/internal/gkr/bls24-315/solver_hints.go b/internal/gkr/bls24-315/solver_hints.go new file mode 100644 index 00000000..914c8a9d --- /dev/null +++ b/internal/gkr/bls24-315/solver_hints.go @@ -0,0 +1,146 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark-crypto/hash" + "github.com/consensys/gnark-crypto/utils" + hint "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + algo_utils "github.com/consensys/gnark/internal/utils" + + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +type SolvingData struct { + assignment WireAssignment + circuit gkrtypes.Circuit + workers *utils.WorkerPool +} + +func (d *SolvingData) init(info gkrtypes.SolvingInfo) { + d.workers = utils.NewWorkerPool() + d.circuit = info.Circuit + d.circuit.SetNbUniqueOutputs() + + d.assignment = make(WireAssignment, len(d.circuit)) + for i := range d.assignment { + d.assignment[i] = make([]fr.Element, info.NbInstances) + } +} + +// this module assumes that wire and instance indexes respect dependencies + +func setOuts(a WireAssignment, circuit gkrtypes.Circuit, outs []*big.Int) { + outsI := 0 + for i := range circuit { + if circuit[i].IsOutput() { + for j := range a[i] { + a[i][j].BigInt(outs[outsI]) + outsI++ + } + } + } + // Check if outsI == len(outs)? +} + +func SolveHint(info gkrtypes.SolvingInfo, data *SolvingData) hint.Hint { + return func(_ *big.Int, ins, outs []*big.Int) error { + // assumes assignmentVector is arranged wire first, instance second in order of solution + offsets := info.AssignmentOffsets() + data.init(info) + maxNIn := data.circuit.MaxGateNbIn() + + chunks := info.Chunks() + + solveTask := func(chunkOffset int) utils.Task { + return func(startInChunk, endInChunk int) { + start := startInChunk + chunkOffset + end := endInChunk + chunkOffset + inputs := make([]frontend.Variable, maxNIn) + dependencyHeads := make([]int, len(data.circuit)) // for each wire, which of its dependencies we would look at next + for wI := range data.circuit { // skip instances that are not relevant (related to instances before the current task) + deps := info.Dependencies[wI] + dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { + return deps[i].InputInstance + }, len(deps), start) + } + + for instanceI := start; instanceI < end; instanceI++ { + for wireI := range data.circuit { + wire := &data.circuit[wireI] + deps := info.Dependencies[wireI] + if wire.IsInput() { + if dependencyHeads[wireI] < len(deps) && instanceI == deps[dependencyHeads[wireI]].InputInstance { + dep := deps[dependencyHeads[wireI]] + data.assignment[wireI][instanceI].Set(&data.assignment[dep.OutputWire][dep.OutputInstance]) + dependencyHeads[wireI]++ + } else { + data.assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) + } + } else { + // assemble the inputs + inputIndexes := info.Circuit[wireI].Inputs + for i, inputI := range inputIndexes { + inputs[i] = &data.assignment[inputI][instanceI] + } + gate := data.circuit[wireI].Gate + data.assignment[wireI][instanceI].Set(gate.Evaluate(api, inputs[:len(inputIndexes)]...).(*fr.Element)) + } + } + } + } + } + + start := 0 + for _, end := range chunks { + data.workers.Submit(end-start, solveTask(start), 1024).Wait() + start = end + } + + for _, p := range info.Prints { + serializable := make([]any, len(p.Values)) + for i, v := range p.Values { + if p.IsGkrVar[i] { // serializer stores uint32 in slices as uint64 + serializable[i] = data.assignment[algo_utils.ForceUint32(v)][p.Instance].String() + } else { + serializable[i] = v + } + } + fmt.Println(serializable...) + } + + setOuts(data.assignment, info.Circuit, outs) + + return nil + } +} + +func ProveHint(hashName string, data *SolvingData) hint.Hint { + + return func(_ *big.Int, ins, outs []*big.Int) error { + insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called + b := make([]byte, fr.Bytes) + i.FillBytes(b) + return b[:] + }) + + hsh := hash.NewHash(hashName + "_BLS24_315") + + proof, err := Prove(data.circuit, data.assignment, fiatshamir.WithHash(hsh, insBytes...), WithWorkers(data.workers)) + if err != nil { + return err + } + + return proof.SerializeToBigInts(outs) + + } +} diff --git a/internal/gkr/bls24-315/sumcheck.go b/internal/gkr/bls24-315/sumcheck.go new file mode 100644 index 00000000..badfd3e0 --- /dev/null +++ b/internal/gkr/bls24-315/sumcheck.go @@ -0,0 +1,171 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "strconv" + + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +// This does not make use of parallelism and represents polynomials as lists of coefficients +// It is currently geared towards arithmetic hashes. Once we have a more unified hash function interface, this can be generified. + +// sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. +// Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) +type sumcheckClaims interface { + combine(a fr.Element) polynomial.Polynomial // combine into the 0ᵗʰ sumcheck subclaim. Create g := ∑_{1≤j≤m} aʲ⁻¹fⱼ for which now we seek to prove ∑_{0≤i<2ⁿ} g(i) = c := ∑_{1≤j≤m} aʲ⁻¹cⱼ. Return g₁. + next(fr.Element) polynomial.Polynomial // Return the evaluations gⱼ(k) for 1 ≤ k < degⱼ(g). Update the claim to gⱼ₊₁ for the input value as rⱼ + varsNum() int // number of variables + claimsNum() int // number of claims + proveFinalEval(r []fr.Element) []fr.Element // in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +// sumcheckLazyClaims is the sumcheckClaims data structure on the verifier side. It is "lazy" in that it has to compute fewer things. +type sumcheckLazyClaims interface { + claimsNum() int // claimsNum = m + varsNum() int // varsNum = n + combinedSum(a fr.Element) fr.Element // combinedSum returns c = ∑_{1≤j≤m} aʲ⁻¹cⱼ + degree(i int) int // degree of the total claim in the i'th variable + verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error +} + +// sumcheckProof of a multi-statement. +type sumcheckProof struct { + partialSumPolys []polynomial.Polynomial + finalEvalProof []fr.Element //in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +func setupTranscript(claimsNum int, varsNum int, settings *fiatshamir.Settings) (challengeNames []string, err error) { + numChallenges := varsNum + if claimsNum >= 2 { + numChallenges++ + } + challengeNames = make([]string, numChallenges) + if claimsNum >= 2 { + challengeNames[0] = settings.Prefix + "comb" + } + prefix := settings.Prefix + "pSP." + for i := 0; i < varsNum; i++ { + challengeNames[i+numChallenges-varsNum] = prefix + strconv.Itoa(i) + } + if settings.Transcript == nil { + transcript := fiatshamir.NewTranscript(settings.Hash, challengeNames...) + settings.Transcript = transcript + } + + for i := range settings.BaseChallenges { + if err = settings.Transcript.Bind(challengeNames[0], settings.BaseChallenges[i]); err != nil { + return + } + } + return +} + +func next(transcript *fiatshamir.Transcript, bindings []fr.Element, remainingChallengeNames *[]string) (fr.Element, error) { + challengeName := (*remainingChallengeNames)[0] + for i := range bindings { + bytes := bindings[i].Bytes() + if err := transcript.Bind(challengeName, bytes[:]); err != nil { + return fr.Element{}, err + } + } + var res fr.Element + bytes, err := transcript.ComputeChallenge(challengeName) + res.SetBytes(bytes) + + *remainingChallengeNames = (*remainingChallengeNames)[1:] + + return res, err +} + +// sumcheckProve create a non-interactive proof +func sumcheckProve(claims sumcheckClaims, transcriptSettings fiatshamir.Settings) (sumcheckProof, error) { + + var proof sumcheckProof + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return proof, err + } + + var combinationCoeff fr.Element + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return proof, err + } + } + + varsNum := claims.varsNum() + proof.partialSumPolys = make([]polynomial.Polynomial, varsNum) + proof.partialSumPolys[0] = claims.combine(combinationCoeff) + challenges := make([]fr.Element, varsNum) + + for j := 0; j+1 < varsNum; j++ { + if challenges[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return proof, err + } + proof.partialSumPolys[j+1] = claims.next(challenges[j]) + } + + if challenges[varsNum-1], err = next(transcript, proof.partialSumPolys[varsNum-1], &remainingChallengeNames); err != nil { + return proof, err + } + + proof.finalEvalProof = claims.proveFinalEval(challenges) + + return proof, nil +} + +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, transcriptSettings fiatshamir.Settings) error { + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return err + } + + var combinationCoeff fr.Element + + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return err + } + } + + r := make([]fr.Element, claims.varsNum()) + + // Just so that there is enough room for gJ to be reused + maxDegree := claims.degree(0) + for j := 1; j < claims.varsNum(); j++ { + if d := claims.degree(j); d > maxDegree { + maxDegree = d + } + } + gJ := make(polynomial.Polynomial, maxDegree+1) //At the end of iteration j, gJ = ∑_{i < 2ⁿ⁻ʲ⁻¹} g(X₁, ..., Xⱼ₊₁, i...) NOTE: n is shorthand for claims.varsNum() + gJR := claims.combinedSum(combinationCoeff) // At the beginning of iteration j, gJR = ∑_{i < 2ⁿ⁻ʲ} g(r₁, ..., rⱼ, i...) + + for j := range claims.varsNum() { + if len(proof.partialSumPolys[j]) != claims.degree(j) { + return errors.New("malformed proof") + } + copy(gJ[1:], proof.partialSumPolys[j]) + gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) // Requirement that gⱼ(0) + gⱼ(1) = gⱼ₋₁(r) + // gJ is ready + + //Prepare for the next iteration + if r[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return err + } + // This is an extremely inefficient way of interpolating. TODO: Interpolate without symbolically computing a polynomial + gJCoeffs := polynomial.InterpolateOnRange(gJ[:(claims.degree(j) + 1)]) + gJR = gJCoeffs.Eval(&r[j]) + } + + return claims.verifyFinalEval(r, combinationCoeff, gJR, proof.finalEvalProof) +} diff --git a/internal/gkr/bls24-315/sumcheck_test.go b/internal/gkr/bls24-315/sumcheck_test.go new file mode 100644 index 00000000..d8ebd295 --- /dev/null +++ b/internal/gkr/bls24-315/sumcheck_test.go @@ -0,0 +1,152 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/stretchr/testify/assert" + + "math/bits" + + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" + + "strings" + "testing" +) + +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { + poly := make(polynomial.MultiLin, len(polyInt)) + for i, n := range polyInt { + poly[i].SetUint64(n) + } + + claim := singleMultilinClaim{g: poly.Clone()} + + proof, err := sumcheckProve(&claim, fiatshamir.WithHash(hashGenerator())) + if err != nil { + return err + } + + var sb strings.Builder + for _, p := range proof.partialSumPolys { + + sb.WriteString("\t{") + for i := 0; i < len(p); i++ { + sb.WriteString(p[i].String()) + if i+1 < len(p) { + sb.WriteString(", ") + } + } + sb.WriteString("}\n") + } + + lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if err = sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())); err != nil { + return err + } + + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())) == nil { + return fmt.Errorf("bad proof accepted") + } + return nil +} + +func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { + + polys := [][]uint64{ + {1, 2, 3, 4}, // 1 + 2X₁ + X₂ + {1, 2, 3, 4, 5, 6, 7, 8}, // 1 + 4X₁ + 2X₂ + X₃ + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, // 1 + 8X₁ + 4X₂ + 2X₃ + X₄ + } + + const MaxStep = 4 + const MaxStart = 4 + hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + + for step := 0; step < MaxStep; step++ { + for startState := 0; startState < MaxStart; startState++ { + if step == 0 && startState == 1 { // unlucky case where a bad proof would be accepted + continue + } + hashGens = append(hashGens, newMessageCounterGenerator(startState, step)) + } + } + + for _, poly := range polys { + for _, hashGen := range hashGens { + assert.NoError(t, testSumcheckSingleClaimMultilin(poly, hashGen), + "failed with poly %v and hashGen %v", poly, hashGen()) + } + } +} + +type singleMultilinClaim struct { + g polynomial.MultiLin +} + +func (c singleMultilinClaim) proveFinalEval(r []fr.Element) []fr.Element { + return nil // verifier can compute the final eval itself +} + +func (c singleMultilinClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} + +func (c singleMultilinClaim) claimsNum() int { + return 1 +} + +func sumForX1One(g polynomial.MultiLin) polynomial.Polynomial { + sum := g[len(g)/2] + for i := len(g)/2 + 1; i < len(g); i++ { + sum.Add(&sum, &g[i]) + } + return []fr.Element{sum} +} + +func (c singleMultilinClaim) combine(fr.Element) polynomial.Polynomial { + return sumForX1One(c.g) +} + +func (c *singleMultilinClaim) next(r fr.Element) polynomial.Polynomial { + c.g.Fold(r) + return sumForX1One(c.g) +} + +type singleMultilinLazyClaim struct { + g polynomial.MultiLin + claimedSum fr.Element +} + +func (c singleMultilinLazyClaim) verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error { + val := c.g.Evaluate(r, nil) + if val.Equal(&purportedValue) { + return nil + } + return fmt.Errorf("mismatch") +} + +func (c singleMultilinLazyClaim) combinedSum(combinationCoeffs fr.Element) fr.Element { + return c.claimedSum +} + +func (c singleMultilinLazyClaim) degree(i int) int { + return 1 +} + +func (c singleMultilinLazyClaim) claimsNum() int { + return 1 +} + +func (c singleMultilinLazyClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} diff --git a/internal/gkr/bls24-315/test_vector_utils_test.go b/internal/gkr/bls24-315/test_vector_utils_test.go new file mode 100644 index 00000000..a4407373 --- /dev/null +++ b/internal/gkr/bls24-315/test_vector_utils_test.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "strings" + + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" + "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrtesting" +) + +func toElement(i int64) *fr.Element { + var res fr.Element + res.SetInt64(i) + return &res +} + +func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { + if _type, ok := d["type"]; ok { + switch _type { + case "const": + startState := int64(d["val"].(float64)) + return &messageCounter{startState: startState, step: 0, state: startState}, nil + default: + return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) + } + } + return nil, fmt.Errorf("hash description missing type") +} + +type messageCounter struct { + startState int64 + state int64 + step int64 +} + +func (m *messageCounter) Write(p []byte) (n int, err error) { + inputBlockSize := (len(p)-1)/fr.Bytes + 1 + m.state += int64(inputBlockSize) * m.step + return len(p), nil +} + +func (m *messageCounter) Sum(b []byte) []byte { + inputBlockSize := (len(b)-1)/fr.Bytes + 1 + resI := m.state + int64(inputBlockSize)*m.step + var res fr.Element + res.SetInt64(int64(resI)) + resBytes := res.Bytes() + return resBytes[:] +} + +func (m *messageCounter) Reset() { + m.state = m.startState +} + +func (m *messageCounter) Size() int { + return fr.Bytes +} + +func (m *messageCounter) BlockSize() int { + return fr.Bytes +} + +func newMessageCounter(startState, step int) hash.Hash { + transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} + return transcript +} + +func newMessageCounterGenerator(startState, step int) func() hash.Hash { + return func() hash.Hash { + return newMessageCounter(startState, step) + } +} + +func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { + + // TODO: Put this in element.SetString? + switch v := value.(type) { + case string: + + if sep := strings.Split(v, "/"); len(sep) == 2 { + var denom fr.Element + if _, err := z.SetString(sep[0]); err != nil { + return nil, err + } + if _, err := denom.SetString(sep[1]); err != nil { + return nil, err + } + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil + } + + case float64: + asInt := int64(v) + if float64(asInt) != v { + return nil, fmt.Errorf("cannot currently parse float") + } + z.SetInt64(asInt) + return z, nil + } + + return z.SetInterface(value) +} + +func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { + elementSlice := make([]fr.Element, len(slice)) + for i, v := range slice { + if _, err := setElement(&elementSlice[i], v); err != nil { + return nil, err + } + } + return elementSlice, nil +} + +func sliceEquals(a []fr.Element, b []fr.Element) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if !a[i].Equal(&b[i]) { + return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) + } + } + return nil +} + +func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if err := sliceEquals(a[i], b[i]); err != nil { + return fmt.Errorf("at index %d: %w", i, err) + } + } + return nil +} diff --git a/internal/gkr/bls24-317/gate_testing.go b/internal/gkr/bls24-317/gate_testing.go new file mode 100644 index 00000000..1bffab29 --- /dev/null +++ b/internal/gkr/bls24-317/gate_testing.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + + "slices" + + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/polynomial" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// IsGateFunctionAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func IsGateFunctionAdditive(f gkr.GateFunction, i, nbIn int) bool { + fWrapped := api.convertFunc(f) + + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 := fWrapped(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := fWrapped(in...) + + y2.Sub(y2, y1) + y1.Sub(y1, y0) + + if !y2.Equal(y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 = fWrapped(in...) + + y1.Sub(y1, y0) + + return y1.Equal(y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f gateFunctionFr) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i].Set(f(fIn...)) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +// FindGateFunctionDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func FindGateFunctionDegree(f gkr.GateFunction, max, nbIn int) (int, error) { + fFr := api.convertFunc(f) + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := fFr.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, gkrtypes.ErrZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func VerifyGateFunctionDegree(f gkr.GateFunction, claimedDegree, nbIn int) error { + fFr := api.convertFunc(f) + if p := fFr.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return gkrtypes.ErrZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} diff --git a/internal/gkr/bls24-317/gkr.go b/internal/gkr/bls24-317/gkr.go new file mode 100644 index 00000000..fc9908b9 --- /dev/null +++ b/internal/gkr/bls24-317/gkr.go @@ -0,0 +1,816 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "sync" + + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// The goal is to prove/verify evaluations of many instances of the same circuit + +// WireAssignment is assignment of values to the same wire across many instances of the circuit +type WireAssignment []polynomial.MultiLin + +type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) + +// eqTimesGateEvalSumcheckLazyClaims is a lazy claim for sumcheck (verifier side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the checking of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckLazyClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ), allegedly + manager *claimsManager // WARNING: Circular references +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) getWire() *gkrtypes.Wire { + return e.manager.wires[e.wireI] +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) claimsNum() int { + return len(e.evaluationPoints) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) varsNum() int { + return len(e.evaluationPoints[0]) +} + +// combinedSum returns ∑ᵢ aⁱ yᵢ +func (e *eqTimesGateEvalSumcheckLazyClaims) combinedSum(a fr.Element) fr.Element { + evalsAsPoly := polynomial.Polynomial(e.claimedEvaluations) + return evalsAsPoly.Eval(&a) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) degree(int) int { + return 1 + e.manager.wires[e.wireI].Gate.Degree() +} + +// verifyFinalEval finalizes the verification of w. +// The prover's claims w(xᵢ) = yᵢ have already been reduced to verifying +// ∑ cⁱ eq(xᵢ, r) w(r) = purportedValue. ( c is combinationCoeff ) +// Both purportedValue and the vector r have been randomized during the sumcheck protocol. +// By taking the w term out of the sum we get the equivalent claim that +// for E := ∑ eq(xᵢ, r), it must be that E w(r) = purportedValue. +// If w is an input wire, the verifier can directly check its evaluation at r. +// Otherwise, the prover makes claims about the evaluation of w's input wires, +// wᵢ, at r, to be verified later. +// The claims are communicated through the proof parameter. +// The verifier checks here if the claimed evaluations of wᵢ(r) are consistent with +// the main claim, by checking E w(wᵢ(r)...) = purportedValue. +func (e *eqTimesGateEvalSumcheckLazyClaims) verifyFinalEval(r []fr.Element, combinationCoeff, purportedValue fr.Element, uniqueInputEvaluations []fr.Element) error { + // the eq terms ( E ) + numClaims := len(e.evaluationPoints) + evaluation := polynomial.EvalEq(e.evaluationPoints[numClaims-1], r) + for i := numClaims - 2; i >= 0; i-- { + evaluation.Mul(&evaluation, &combinationCoeff) + eq := polynomial.EvalEq(e.evaluationPoints[i], r) + evaluation.Add(&evaluation, &eq) + } + + wire := e.manager.wires[e.wireI] + + // the w(...) term + var gateEvaluation fr.Element + if wire.IsInput() { // just compute w(r) + gateEvaluation = e.manager.assignment[e.wireI].Evaluate(r, e.manager.memPool) + } else { // proof contains the evaluations of the inputs, but avoids repetition in case multiple inputs come from the same wire + injection, injectionLeftInv := + e.manager.wires.ClaimPropagationInfo(e.wireI) + + if len(injection) != len(uniqueInputEvaluations) { + return fmt.Errorf("%d input wire evaluations given, %d expected", len(uniqueInputEvaluations), len(injection)) + } + + for uniqueI, i := range injection { // map from unique to all + e.manager.add(wire.Inputs[i], r, uniqueInputEvaluations[uniqueI]) + } + + inputEvaluations := make([]frontend.Variable, len(wire.Inputs)) + for i, uniqueI := range injectionLeftInv { // map from all to unique + inputEvaluations[i] = &uniqueInputEvaluations[uniqueI] + } + + gateEvaluation.Set(wire.Gate.Evaluate(api, inputEvaluations...).(*fr.Element)) + } + + evaluation.Mul(&evaluation, &gateEvaluation) + + if evaluation.Equal(&purportedValue) { + return nil + } + return errors.New("incompatible evaluations") +} + +// eqTimesGateEvalSumcheckClaims is a claim for sumcheck (prover side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the proving of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ) + manager *claimsManager + + input []polynomial.MultiLin // input[i](h₁, ..., hₘ₋ⱼ) = wᵢ(r₁, r₂, ..., rⱼ₋₁, h₁, ..., hₘ₋ⱼ) + + eq polynomial.MultiLin // E := ∑ᵢ cⁱ eq(xᵢ, -) +} + +func (c *eqTimesGateEvalSumcheckClaims) getWire() *gkrtypes.Wire { + return c.manager.wires[c.wireI] +} + +// combine the multiple claims into one claim using a random combination (combinationCoeff or c). +// From the original multiple claims of w(xᵢ) = yᵢ, we get a single claim +// ∑ᵢ,ₕ cⁱ eq(xᵢ, h) w(h) = ∑ᵢ cⁱ yᵢ, where h iterates over the hypercube (circuit instances) and +// i iterates over the claims. +// Equivalently, we could say ∑ᵢ cⁱ yᵢ = ∑ₕ,ᵢ cⁱ eq(xᵢ, h) w(h) = ∑ₕ w(h) ∑ᵢ cⁱ eq(xᵢ, h). +// Thus if we initially compute E := ∑ᵢ cⁱ eq(xᵢ, -), our claim will find the simpler form +// ∑ᵢ cⁱ yᵢ = ∑ₕ w(h) E(h), where the sum-checked polynomial is of degree deg(g) + 1, +// and deg(g) is the total degree of the polynomial defining the gate g of which w is the output. +// The output of combine is the first sumcheck claim, i.e. ∑₍ₕ₁,ₕ₂,...₎ w(X, h₁, h₂, ...) E(X, h₁, h₂, ...).. +func (c *eqTimesGateEvalSumcheckClaims) combine(combinationCoeff fr.Element) polynomial.Polynomial { + varsNum := c.varsNum() + eqLength := 1 << varsNum + claimsNum := c.claimsNum() + // initialize the eq tables ( E ) + c.eq = c.manager.memPool.Make(eqLength) + + c.eq[0].SetOne() + c.eq.Eq(c.evaluationPoints[0]) + + // E := eq(x₀, -) + newEq := polynomial.MultiLin(c.manager.memPool.Make(eqLength)) + aI := combinationCoeff + + // E += cⁱ eq(xᵢ, -) + for k := 1; k < claimsNum; k++ { + newEq[0].Set(&aI) + + c.eqAcc(c.eq, newEq, c.evaluationPoints[k]) + + if k+1 < claimsNum { + aI.Mul(&aI, &combinationCoeff) + } + } + + c.manager.memPool.Dump(newEq) + + return c.computeGJ() +} + +// eqAcc sets m to an eq table at q and then adds it to e. +// m <- eq(q, -). +// e <- e + m +func (c *eqTimesGateEvalSumcheckClaims) eqAcc(e, m polynomial.MultiLin, q []fr.Element) { + n := len(q) + + //At the end of each iteration, m(h₁, ..., hₙ) = eq(q₁, ..., qᵢ₊₁, h₁, ..., hᵢ₊₁) + for i := range q { // In the comments we use a 1-based index so q[i] = qᵢ₊₁ + // go through all assignments of (b₁, ..., bᵢ) ∈ {0,1}ⁱ + const threshold = 1 << 6 + k := 1 << i + if k < threshold { + for j := 0; j < k; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + } else { + c.manager.workers.Submit(k, func(start, end int) { + for j := start; j < end; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + }, 1024).Wait() + } + + } + c.manager.workers.Submit(len(e), func(start, end int) { + for i := start; i < end; i++ { + e[i].Add(&e[i], &m[i]) + } + }, 512).Wait() +} + +// computeGJ: gⱼ = ∑_{0≤h<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, h...) = ∑_{0≤i<2ⁿ⁻ʲ} E(r₁, ..., Xⱼ, h...) g( w₀(r₁, ..., Xⱼ, h...), ... ). +// the polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). +// The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). By convention, g₀ is a constant polynomial equal to the claimed sum. +func (c *eqTimesGateEvalSumcheckClaims) computeGJ() polynomial.Polynomial { + + wire := c.getWire() + degGJ := 1 + wire.Gate.Degree() // guaranteed to be no smaller than the actual deg(gⱼ) + nbGateIn := len(c.input) + + // Both E and wᵢ (the input wires and the eq table) are multilinear, thus + // they are linear in Xⱼ. + // So for f ∈ { E(r₁, ..., Xⱼ, h...) } ∪ {wᵢ(r₁, ..., Xⱼ, h...) }, so f(m) = m×(f(1) - f(0)) + f(0), and f(0), f(1) are easily computed from the bookkeeping tables. + // ml are such multilinear polynomials the evaluations of which over different values of Xⱼ are computed in this stepwise manner. + ml := make([]polynomial.MultiLin, nbGateIn+1) // shortcut to the evaluations of the multilinear polynomials over the hypercube + ml[0] = c.eq + copy(ml[1:], c.input) + + sumSize := len(c.eq) / 2 // the range of h, over which we sum + + // Perf-TODO: Collate once at claim "combination" time and not again. then, even folding can be done in one operation every time "next" is called + + gJ := make([]fr.Element, degGJ) + var mu sync.Mutex + computeAll := func(start, end int) { // compute method to allow parallelization across instances + var step fr.Element + + res := make([]fr.Element, degGJ) + + // evaluations of ml, laid out as: + // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), + // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), + // ... + // ml[0](degGJ, h...), ml[2](degGJ, h...), ..., ml[len(ml)-1](degGJ, h...) + mlEvals := make([]fr.Element, degGJ*len(ml)) + gateInput := make([]frontend.Variable, nbGateIn) + + for h := start; h < end; h++ { // h counts across instances + + evalAt1Index := sumSize + h + for k := range ml { + // d = 0 + mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1. Can be taken directly from the table. + step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for d := 1; d < degGJ; d++ { + mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + } + } + + eIndex := 0 // index for where the current eq term is + nextEIndex := len(ml) + for d := range degGJ { + for i := range gateInput { + gateInput[i] = &mlEvals[eIndex+1+i] + } + summand := wire.Gate.Evaluate(api, gateInput...).(*fr.Element) + summand.Mul(summand, &mlEvals[eIndex]) + res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + } + } + mu.Lock() + for i := range gJ { + gJ[i].Add(&gJ[i], &res[i]) // collect into the complete sum + } + mu.Unlock() + } + + const minBlockSize = 64 + + if sumSize < minBlockSize { + // no parallelization + computeAll(0, sumSize) + } else { + c.manager.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } + + return gJ +} + +// next first folds the input and E polynomials at the given verifier challenge then computes the new gⱼ. +// Thus, j <- j+1 and rⱼ = challenge. +func (c *eqTimesGateEvalSumcheckClaims) next(challenge fr.Element) polynomial.Polynomial { + const minBlockSize = 512 + n := len(c.eq) / 2 + if n < minBlockSize { + // no parallelization + for i := 0; i < len(c.input); i++ { + c.input[i].Fold(challenge) + } + c.eq.Fold(challenge) + } else { + wgs := make([]*sync.WaitGroup, len(c.input)) + for i := 0; i < len(c.input); i++ { + wgs[i] = c.manager.workers.Submit(n, c.input[i].FoldParallel(challenge), minBlockSize) + } + c.manager.workers.Submit(n, c.eq.FoldParallel(challenge), minBlockSize).Wait() + for _, wg := range wgs { + wg.Wait() + } + } + + return c.computeGJ() +} + +func (c *eqTimesGateEvalSumcheckClaims) varsNum() int { + return len(c.evaluationPoints[0]) +} + +func (c *eqTimesGateEvalSumcheckClaims) claimsNum() int { + return len(c.claimedEvaluations) +} + +// proveFinalEval provides the values wᵢ(r₁, ..., rₙ) +func (c *eqTimesGateEvalSumcheckClaims) proveFinalEval(r []fr.Element) []fr.Element { + //defer the proof, return list of claims + + injection, _ := c.manager.wires.ClaimPropagationInfo(c.wireI) // TODO @Tabaie: Instead of doing this last, we could just have fewer input in the first place; not that likely to happen with single gates, but more so with layers. + evaluations := make([]fr.Element, len(injection)) + for i, gateInputI := range injection { + wI := c.input[gateInputI] + wI.Fold(r[len(r)-1]) // We already have wᵢ(r₁, ..., rₙ₋₁, hₙ) in a table. Only one more fold required. + c.manager.add(c.getWire().Inputs[gateInputI], r, wI[0]) + evaluations[i] = wI[0] + } + + c.manager.memPool.Dump(c.claimedEvaluations, c.eq) + + return evaluations +} + +type claimsManager struct { + claims []*eqTimesGateEvalSumcheckLazyClaims + assignment WireAssignment + memPool *polynomial.Pool + workers *utils.WorkerPool + wires gkrtypes.Wires +} + +func newClaimsManager(wires []*gkrtypes.Wire, assignment WireAssignment, o settings) (manager claimsManager) { + manager.assignment = assignment + manager.claims = make([]*eqTimesGateEvalSumcheckLazyClaims, len(wires)) + manager.memPool = o.pool + manager.workers = o.workers + manager.wires = wires + + for i, wire := range wires { + + manager.claims[i] = &eqTimesGateEvalSumcheckLazyClaims{ + wireI: i, + evaluationPoints: make([][]fr.Element, 0, wire.NbClaims()), + claimedEvaluations: manager.memPool.Make(wire.NbClaims()), + manager: &manager, + } + } + return +} + +func (m *claimsManager) add(wire int, evaluationPoint []fr.Element, evaluation fr.Element) { + claim := m.claims[wire] + i := len(claim.evaluationPoints) + claim.claimedEvaluations[i] = evaluation + claim.evaluationPoints = append(claim.evaluationPoints, evaluationPoint) +} + +func (m *claimsManager) getLazyClaim(wire int) *eqTimesGateEvalSumcheckLazyClaims { + return m.claims[wire] +} + +func (m *claimsManager) getClaim(wireI int) *eqTimesGateEvalSumcheckClaims { + lazy := m.claims[wireI] + wire := m.wires[wireI] + res := &eqTimesGateEvalSumcheckClaims{ + wireI: wireI, + evaluationPoints: lazy.evaluationPoints, + claimedEvaluations: lazy.claimedEvaluations, + manager: m, + } + + if wire.IsInput() { + res.input = []polynomial.MultiLin{m.memPool.Clone(m.assignment[wireI])} + } else { + res.input = make([]polynomial.MultiLin, len(wire.Inputs)) + + for inputI, inputW := range wire.Inputs { + res.input[inputI] = m.memPool.Clone(m.assignment[inputW]) //will be edited later, so must be deep copied + } + } + return res +} + +func (m *claimsManager) deleteClaim(wire int) { + m.claims[wire].manager = nil + m.claims[wire] = nil +} + +type settings struct { + pool *polynomial.Pool + sorted []*gkrtypes.Wire + transcript *fiatshamir.Transcript + transcriptPrefix string + nbVars int + workers *utils.WorkerPool +} + +type Option func(*settings) + +func WithPool(pool *polynomial.Pool) Option { + return func(options *settings) { + options.pool = pool + } +} + +func WithSortedCircuit(sorted []*gkrtypes.Wire) Option { + return func(options *settings) { + options.sorted = sorted + } +} + +func WithWorkers(workers *utils.WorkerPool) Option { + return func(options *settings) { + options.workers = workers + } +} + +func setup(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (settings, error) { + var o settings + var err error + for _, option := range options { + option(&o) + } + + o.nbVars = assignment.NumVars() + nbInstances := assignment.NumInstances() + if 1< 1 { //combine the claims + size++ + } + size += logNbInstances // full run of sumcheck on logNbInstances variables + } + + nums := make([]string, max(len(sorted), logNbInstances)) + for i := range nums { + nums[i] = strconv.Itoa(i) + } + + challenges := make([]string, size) + + // output wire claims + firstChallengePrefix := prefix + "fC." + for j := 0; j < logNbInstances; j++ { + challenges[j] = firstChallengePrefix + nums[j] + } + j := logNbInstances + for i := len(sorted) - 1; i >= 0; i-- { + if sorted[i].NoProof() { + continue + } + wirePrefix := prefix + "w" + nums[i] + "." + + if sorted[i].NbClaims() > 1 { + challenges[j] = wirePrefix + "comb" + j++ + } + + partialSumPrefix := wirePrefix + "pSP." + for k := 0; k < logNbInstances; k++ { + challenges[j] = partialSumPrefix + nums[k] + j++ + } + } + return challenges +} + +func getFirstChallengeNames(logNbInstances int, prefix string) []string { + res := make([]string, logNbInstances) + firstChallengePrefix := prefix + "fC." + for i := 0; i < logNbInstances; i++ { + res[i] = firstChallengePrefix + strconv.Itoa(i) + } + return res +} + +func getChallenges(transcript *fiatshamir.Transcript, names []string) ([]fr.Element, error) { + res := make([]fr.Element, len(names)) + for i, name := range names { + if bytes, err := transcript.ComputeChallenge(name); err == nil { + res[i].SetBytes(bytes) + } else { + return nil, err + } + } + return res, nil +} + +// Prove consistency of the claimed assignment +func Prove(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (Proof, error) { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return nil, err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + proof := make(Proof, len(c)) + // firstChallenge called rho in the paper + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return nil, err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + claim := claims.getClaim(i) + if wire.NoProof() { // input wires with one claim only + proof[i] = sumcheckProof{ + partialSumPolys: []polynomial.Polynomial{}, + finalEvalProof: []fr.Element{}, + } + } else { + if proof[i], err = sumcheckProve( + claim, fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err != nil { + return proof, err + } + + baseChallenge = make([][]byte, len(proof[i].finalEvalProof)) + for j := range proof[i].finalEvalProof { + baseChallenge[j] = proof[i].finalEvalProof[j].Marshal() + } + } + // the verifier checks a single claim about input wires itself + claims.deleteClaim(i) + } + + return proof, nil +} + +// Verify the consistency of the claimed output with the claimed input +// Unlike in Prove, the assignment argument need not be complete +func Verify(c gkrtypes.Circuit, assignment WireAssignment, proof Proof, transcriptSettings fiatshamir.Settings, options ...Option) error { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + proofW := proof[i] + claim := claims.getLazyClaim(i) + if wire.NoProof() { // input wires with one claim only + // make sure the proof is empty + if len(proofW.finalEvalProof) != 0 || len(proofW.partialSumPolys) != 0 { + return errors.New("no proof allowed for input wire with a single claim") + } + + if wire.NbClaims() == 1 { // input wire + // simply evaluate and see if it matches + evaluation := assignment[i].Evaluate(claim.evaluationPoints[0], claims.memPool) + if !claim.claimedEvaluations[0].Equal(&evaluation) { + return errors.New("incorrect input wire claim") + } + } + } else if err = sumcheckVerify( + claim, proof[i], fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err == nil { // incorporate prover claims about w's input into the transcript + baseChallenge = make([][]byte, len(proofW.finalEvalProof)) + for j := range baseChallenge { + baseChallenge[j] = proofW.finalEvalProof[j].Marshal() + } + } else { + return fmt.Errorf("sumcheck proof rejected: %v", err) //TODO: Any polynomials to dump? + } + claims.deleteClaim(i) + } + return nil +} + +// Complete the circuit evaluation from input values +func (a WireAssignment) Complete(wires gkrtypes.Wires) WireAssignment { + + nbInstances := a.NumInstances() + maxNbIns := 0 + + for i, w := range wires { + maxNbIns = max(maxNbIns, len(w.Inputs)) + if len(a[i]) != nbInstances { + a[i] = make([]fr.Element, nbInstances) + } + } + + ins := make([]fr.Element, maxNbIns) + for i := range nbInstances { + for wI, w := range wires { + if !w.IsInput() { + for inI, in := range w.Inputs { + ins[inI] = a[in][i] + } + a[wI][i].Set(api.evaluate(w.Gate.Evaluate, ins[:len(w.Inputs)]...)) + } + } + } + + return a +} + +func (a WireAssignment) NumInstances() int { + for _, aW := range a { + return len(aW) + } + panic("empty assignment") +} + +func (a WireAssignment) NumVars() int { + for _, aW := range a { + return aW.NumVars() + } + panic("empty assignment") +} + +// SerializeToBigInts flattens a proof object into the given slice of big.Ints +// useful in gnark hints. +func (p Proof) SerializeToBigInts(outs []*big.Int) error { + offset := 0 + for i := range p { + for _, poly := range p[i].partialSumPolys { + frToBigInts(outs[offset:], poly) + offset += len(poly) + } + if p[i].finalEvalProof != nil { + frToBigInts(outs[offset:], p[i].finalEvalProof) + offset += len(p[i].finalEvalProof) + } + } + if offset != len(outs) { + return fmt.Errorf("expected %d elements, got %d", offset, len(outs)) + } + return nil +} + +func frToBigInts(dst []*big.Int, src []fr.Element) { + for i := range src { + src[i].BigInt(dst[i]) + } +} + +// gateAPI implements gkr.GateAPI. +type gateAPI struct{} + +var api gateAPI + +func (gateAPI) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element // TODO Heap allocated. Keep an eye on perf + res.Add(cast(i1), cast(i2)) + for _, v := range in { + res.Add(&res, cast(v)) + } + return &res +} + +func (gateAPI) MulAcc(a, b, c frontend.Variable) frontend.Variable { + var prod fr.Element + prod.Add(cast(b), cast(c)) + res := cast(a) + res.Add(res, &prod) + return &res +} + +func (gateAPI) Neg(i1 frontend.Variable) frontend.Variable { + var res fr.Element + res.Neg(cast(i1)) + return &res +} + +func (gateAPI) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Sub(cast(i1), cast(i2)) + for _, v := range in { + res.Sub(&res, cast(v)) + } + return &res +} + +func (gateAPI) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Mul(cast(i1), cast(i2)) + for _, v := range in { + res.Mul(&res, cast(v)) + } + return &res +} + +func (gateAPI) Println(a ...frontend.Variable) { + toPrint := make([]any, len(a)) + var x fr.Element + + for i, v := range a { + if _, err := x.SetInterface(v); err != nil { + toPrint[i] = x.String() + } else { + if s, ok := v.(string); ok { + toPrint[i] = s + continue + } + panic(fmt.Errorf("not numeric or string: %w", err)) + } + } + fmt.Println(toPrint...) +} + +func (api gateAPI) evaluate(f gkr.GateFunction, in ...fr.Element) *fr.Element { + inVar := make([]frontend.Variable, len(in)) + for i := range in { + inVar[i] = &in[i] + } + return f(api, inVar...).(*fr.Element) +} + +type gateFunctionFr func(...fr.Element) *fr.Element + +// convertFunc turns f into a function that accepts and returns fr.Element. +func (api gateAPI) convertFunc(f gkr.GateFunction) gateFunctionFr { + return func(in ...fr.Element) *fr.Element { + return api.evaluate(f, in...) + } +} + +func cast(v frontend.Variable) *fr.Element { + if x, ok := v.(*fr.Element); ok { // fast path, no extra heap allocation + return x + } + var x fr.Element + if _, err := x.SetInterface(v); err != nil { + panic(err) + } + return &x +} diff --git a/internal/gkr/bls24-317/gkr_test.go b/internal/gkr/bls24-317/gkr_test.go new file mode 100644 index 00000000..eda95888 --- /dev/null +++ b/internal/gkr/bls24-317/gkr_test.go @@ -0,0 +1,505 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "os" + "path/filepath" + "reflect" + "strconv" + "testing" + "time" + + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/mimc" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + gcUtils "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/internal/utils" + "github.com/consensys/gnark/std/gkrapi/gkr" + "github.com/stretchr/testify/assert" +) + +func TestNoGateTwoInstances(t *testing.T) { + // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case + testNoGate(t, []fr.Element{four, three}) +} + +func TestNoGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}}) +} + +func TestSingleAddGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Add2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleMulGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Mul2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleInputTwoIdentityGates(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + }) +} + +func TestSingleInputTwoIdentityGatesComposed(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{1}, + }}) +} + +func TestAPowNTimesBCircuit(t *testing.T) { + const N = 10 + + c := make(gkrtypes.Circuit, N+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: gkrtypes.Mul2(), + Inputs: []int{i - 1, 0}, + } + } + + test(t, c) +} + +func TestSingleMimcCipherGate(t *testing.T) { + test(t, gkrtypes.Circuit{ + {}, {}, + { + Inputs: []int{0, 1}, + Gate: cache.GetGate("mimc"), + }, + }) +} + +func TestShallowMimcTwoInstances(t *testing.T) { + test(t, mimcCircuit(2)) +} + +func TestMimc(t *testing.T) { + test(t, mimcCircuit(93)) +} + +func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { + circuit := gkrtypes.Circuit{gkrtypes.Wire{ + Gate: gkrtypes.Identity(), + NbUniqueOutputs: 2, + }} + + assignment := WireAssignment{[]fr.Element{two, three}} + var o settings + pool := polynomial.NewPool(256, 1<<11) + workers := gcUtils.NewWorkerPool() + o.pool = &pool + o.workers = workers + + claimsManagerGen := func() *claimsManager { + manager := newClaimsManager(utils.References(circuit), assignment, o) + manager.add(0, []fr.Element{three}, five) + manager.add(0, []fr.Element{four}, six) + return &manager + } + + transcriptGen := newMessageCounterGenerator(4, 1) + + proof, err := sumcheckProve(claimsManagerGen().getClaim(0), fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) + err = sumcheckVerify(claimsManagerGen().getLazyClaim(0), proof, fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) +} + +var one, two, three, four, five, six fr.Element + +func init() { + one.SetOne() + two.Double(&one) + three.Add(&two, &one) + four.Double(&two) + five.Add(&three, &two) + six.Double(&three) +} + +var testManyInstancesLogMaxInstances = -1 + +func getLogMaxInstances(t *testing.T) int { + if testManyInstancesLogMaxInstances == -1 { + + s := os.Getenv("GKR_LOG_INSTANCES") + if s == "" { + testManyInstancesLogMaxInstances = 5 + } else { + var err error + testManyInstancesLogMaxInstances, err = strconv.Atoi(s) + if err != nil { + t.Error(err) + } + } + + } + return testManyInstancesLogMaxInstances +} + +func test(t *testing.T, circuit gkrtypes.Circuit) { + wireRefs := utils.References(circuit) + ins := circuit.Inputs() + insAssignment := make(WireAssignment, len(ins)) + maxSize := 1 << getLogMaxInstances(t) + + for i := range ins { + insAssignment[i] = make([]fr.Element, maxSize) + fr.Vector(insAssignment[i]).MustSetRandom() + } + + fullAssignment := make(WireAssignment, len(circuit)) + for _, numEvals := range []int{2, maxSize} { + for i := range ins { + fullAssignment[ins[i]] = insAssignment[i][:numEvals] + } + + fullAssignment.Complete(wireRefs) + + t.Log("Selected inputs for test") + + proof, err := Prove(circuit, fullAssignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") + + if proof.isEmpty() { // special case for TestNoGate: + continue // there's no way to make a trivial proof fail + } + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(0, 1))) + assert.NotNil(t, err, "bad proof accepted") + } + +} + +func (p Proof) isEmpty() bool { + for i := range p { + if len(p[i].finalEvalProof) != 0 { + return false + } + for j := range p[i].partialSumPolys { + if len(p[i].partialSumPolys[j]) != 0 { + return false + } + } + } + return true +} + +func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { + c := gkrtypes.Circuit{ + {}, + } + + assignment := WireAssignment{0: inputAssignments[0]} + + proof, err := Prove(c, assignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(c, assignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") +} + +func mimcCircuit(numRounds int) gkrtypes.Circuit { + c := make(gkrtypes.Circuit, numRounds+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: cache.GetGate("mimc"), + Inputs: []int{i - 1, 0}, + } + } + return c +} + +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("bivariate input needed") + } + res := api.Add(x[0], x[1]) + return api.Mul(res, x[0]) + } + + // g: x,y -> x² + 3y + g := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + res := api.Mul(x[0], x[0]) + y3 := api.Mul(x[1], 3) + return api.Add(res, y3) + } + + // h: x -> 2x + // but it edits it input + h := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + return api.Add(x[0], x[0]) + } + + assert.False(t, IsGateFunctionAdditive(f, 1, 2)) + assert.False(t, IsGateFunctionAdditive(f, 0, 2)) + + assert.False(t, IsGateFunctionAdditive(g, 0, 2)) + assert.True(t, IsGateFunctionAdditive(g, 1, 2)) + + assert.True(t, IsGateFunctionAdditive(h, 0, 1)) +} + +func generateTestProver(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + proof, err := Prove(testCase.Circuit, testCase.FullAssignment, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err) + assert.NoError(t, proofEquals(testCase.Proof, proof)) + } +} + +func generateTestVerifier(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err, "proof rejected") + testCase, err = newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(newMessageCounter(2, 0))) + assert.NotNil(t, err, "bad proof accepted") + } +} + +func TestGkrVectors(t *testing.T) { + + const testDirPath = "../test_vectors/" + dirEntries, err := os.ReadDir(testDirPath) + assert.NoError(t, err) + for _, dirEntry := range dirEntries { + if !dirEntry.IsDir() { + + if filepath.Ext(dirEntry.Name()) == ".json" { + path := filepath.Join(testDirPath, dirEntry.Name()) + noExt := dirEntry.Name()[:len(dirEntry.Name())-len(".json")] + + t.Run(noExt+"_prover", generateTestProver(path)) + t.Run(noExt+"_verifier", generateTestVerifier(path)) + + } + } + } +} + +func proofEquals(expected Proof, seen Proof) error { + if len(expected) != len(seen) { + return fmt.Errorf("length mismatch %d ≠ %d", len(expected), len(seen)) + } + for i, x := range expected { + xSeen := seen[i] + + if xSeen.finalEvalProof == nil { + if seenFinalEval := x.finalEvalProof; len(seenFinalEval) != 0 { + return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) + } + } else { + if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + return fmt.Errorf("final evaluation proof mismatch") + } + } + if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + return err + } + } + return nil +} + +func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { + fmt.Println("creating circuit structure") + c := mimcCircuit(mimcDepth) + + in0 := make([]fr.Element, nbInstances) + in1 := make([]fr.Element, nbInstances) + fr.Vector(in0).MustSetRandom() + fr.Vector(in1).MustSetRandom() + + fmt.Println("evaluating circuit") + start := time.Now().UnixMicro() + assignment := WireAssignment{in0, in1}.Complete(utils.References(c)) + solved := time.Now().UnixMicro() - start + fmt.Println("solved in", solved, "μs") + + //b.ResetTimer() + fmt.Println("constructing proof") + start = time.Now().UnixMicro() + _, err := Prove(c, assignment, fiatshamir.WithHash(mimc.NewMiMC())) + proved := time.Now().UnixMicro() - start + fmt.Println("proved in", proved, "μs") + assert.NoError(b, err) +} + +func BenchmarkGkrMimc19(b *testing.B) { + benchmarkGkrMiMC(b, 1<<19, 91) +} + +func BenchmarkGkrMimc17(b *testing.B) { + benchmarkGkrMiMC(b, 1<<17, 91) +} + +func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { + proof := make(Proof, len(printable)) + for i := range printable { + finalEvalProof := []fr.Element(nil) + + if printable[i].FinalEvalProof != nil { + finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) + finalEvalProof = make([]fr.Element, finalEvalSlice.Len()) + for k := range finalEvalProof { + if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + return nil, err + } + } + } + + proof[i] = sumcheckProof{ + partialSumPolys: make([]polynomial.Polynomial, len(printable[i].PartialSumPolys)), + finalEvalProof: finalEvalProof, + } + for k := range printable[i].PartialSumPolys { + var err error + if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + return nil, err + } + } + } + return proof, nil +} + +type TestCase struct { + Circuit gkrtypes.Circuit + Hash hash.Hash + Proof Proof + FullAssignment WireAssignment + InOutAssignment WireAssignment +} + +var ( + testCases = make(map[string]*TestCase) + cache = gkrtesting.NewCache() +) + +func newTestCase(path string) (*TestCase, error) { + path, err := filepath.Abs(path) + if err != nil { + return nil, err + } + dir := filepath.Dir(path) + + tCase, ok := testCases[path] + if ok { + return tCase, nil + } + + info, err := cache.ReadTestCaseInfo(path) + if err != nil { + return nil, err + } + + circuit := cache.GetCircuit(filepath.Join(dir, info.Circuit)) + var _hash hash.Hash + if _hash, err = hashFromDescription(info.Hash); err != nil { + return nil, err + } + var proof Proof + if proof, err = unmarshalProof(info.Proof); err != nil { + return nil, err + } + + fullAssignment := make(WireAssignment, len(circuit)) + inOutAssignment := make(WireAssignment, len(circuit)) + + sorted := circuit.TopologicalSort() + + inI, outI := 0, 0 + for i, w := range sorted { + var assignmentRaw []interface{} + if w.IsInput() { + if inI == len(info.Input) { + return nil, fmt.Errorf("fewer input in vector than in circuit") + } + assignmentRaw = info.Input[inI] + inI++ + } else if w.IsOutput() { + if outI == len(info.Output) { + return nil, fmt.Errorf("fewer output in vector than in circuit") + } + assignmentRaw = info.Output[outI] + outI++ + } + if assignmentRaw != nil { + var wireAssignment []fr.Element + if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + return nil, err + } + + fullAssignment[i] = wireAssignment + inOutAssignment[i] = wireAssignment + } + } + + fullAssignment.Complete(utils.References(circuit)) + + for i, w := range sorted { + if w.IsOutput() { + if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + return nil, fmt.Errorf("assignment mismatch: %v", err) + } + } + } + + tCase = &TestCase{ + FullAssignment: fullAssignment, + InOutAssignment: inOutAssignment, + Proof: proof, + Hash: _hash, + Circuit: circuit, + } + + testCases[path] = tCase + + return tCase, nil +} diff --git a/internal/gkr/bls24-317/solver_hints.go b/internal/gkr/bls24-317/solver_hints.go new file mode 100644 index 00000000..f6e1ad99 --- /dev/null +++ b/internal/gkr/bls24-317/solver_hints.go @@ -0,0 +1,146 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark-crypto/hash" + "github.com/consensys/gnark-crypto/utils" + hint "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + algo_utils "github.com/consensys/gnark/internal/utils" + + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +type SolvingData struct { + assignment WireAssignment + circuit gkrtypes.Circuit + workers *utils.WorkerPool +} + +func (d *SolvingData) init(info gkrtypes.SolvingInfo) { + d.workers = utils.NewWorkerPool() + d.circuit = info.Circuit + d.circuit.SetNbUniqueOutputs() + + d.assignment = make(WireAssignment, len(d.circuit)) + for i := range d.assignment { + d.assignment[i] = make([]fr.Element, info.NbInstances) + } +} + +// this module assumes that wire and instance indexes respect dependencies + +func setOuts(a WireAssignment, circuit gkrtypes.Circuit, outs []*big.Int) { + outsI := 0 + for i := range circuit { + if circuit[i].IsOutput() { + for j := range a[i] { + a[i][j].BigInt(outs[outsI]) + outsI++ + } + } + } + // Check if outsI == len(outs)? +} + +func SolveHint(info gkrtypes.SolvingInfo, data *SolvingData) hint.Hint { + return func(_ *big.Int, ins, outs []*big.Int) error { + // assumes assignmentVector is arranged wire first, instance second in order of solution + offsets := info.AssignmentOffsets() + data.init(info) + maxNIn := data.circuit.MaxGateNbIn() + + chunks := info.Chunks() + + solveTask := func(chunkOffset int) utils.Task { + return func(startInChunk, endInChunk int) { + start := startInChunk + chunkOffset + end := endInChunk + chunkOffset + inputs := make([]frontend.Variable, maxNIn) + dependencyHeads := make([]int, len(data.circuit)) // for each wire, which of its dependencies we would look at next + for wI := range data.circuit { // skip instances that are not relevant (related to instances before the current task) + deps := info.Dependencies[wI] + dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { + return deps[i].InputInstance + }, len(deps), start) + } + + for instanceI := start; instanceI < end; instanceI++ { + for wireI := range data.circuit { + wire := &data.circuit[wireI] + deps := info.Dependencies[wireI] + if wire.IsInput() { + if dependencyHeads[wireI] < len(deps) && instanceI == deps[dependencyHeads[wireI]].InputInstance { + dep := deps[dependencyHeads[wireI]] + data.assignment[wireI][instanceI].Set(&data.assignment[dep.OutputWire][dep.OutputInstance]) + dependencyHeads[wireI]++ + } else { + data.assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) + } + } else { + // assemble the inputs + inputIndexes := info.Circuit[wireI].Inputs + for i, inputI := range inputIndexes { + inputs[i] = &data.assignment[inputI][instanceI] + } + gate := data.circuit[wireI].Gate + data.assignment[wireI][instanceI].Set(gate.Evaluate(api, inputs[:len(inputIndexes)]...).(*fr.Element)) + } + } + } + } + } + + start := 0 + for _, end := range chunks { + data.workers.Submit(end-start, solveTask(start), 1024).Wait() + start = end + } + + for _, p := range info.Prints { + serializable := make([]any, len(p.Values)) + for i, v := range p.Values { + if p.IsGkrVar[i] { // serializer stores uint32 in slices as uint64 + serializable[i] = data.assignment[algo_utils.ForceUint32(v)][p.Instance].String() + } else { + serializable[i] = v + } + } + fmt.Println(serializable...) + } + + setOuts(data.assignment, info.Circuit, outs) + + return nil + } +} + +func ProveHint(hashName string, data *SolvingData) hint.Hint { + + return func(_ *big.Int, ins, outs []*big.Int) error { + insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called + b := make([]byte, fr.Bytes) + i.FillBytes(b) + return b[:] + }) + + hsh := hash.NewHash(hashName + "_BLS24_317") + + proof, err := Prove(data.circuit, data.assignment, fiatshamir.WithHash(hsh, insBytes...), WithWorkers(data.workers)) + if err != nil { + return err + } + + return proof.SerializeToBigInts(outs) + + } +} diff --git a/internal/gkr/bls24-317/sumcheck.go b/internal/gkr/bls24-317/sumcheck.go new file mode 100644 index 00000000..0ed45d91 --- /dev/null +++ b/internal/gkr/bls24-317/sumcheck.go @@ -0,0 +1,171 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "strconv" + + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +// This does not make use of parallelism and represents polynomials as lists of coefficients +// It is currently geared towards arithmetic hashes. Once we have a more unified hash function interface, this can be generified. + +// sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. +// Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) +type sumcheckClaims interface { + combine(a fr.Element) polynomial.Polynomial // combine into the 0ᵗʰ sumcheck subclaim. Create g := ∑_{1≤j≤m} aʲ⁻¹fⱼ for which now we seek to prove ∑_{0≤i<2ⁿ} g(i) = c := ∑_{1≤j≤m} aʲ⁻¹cⱼ. Return g₁. + next(fr.Element) polynomial.Polynomial // Return the evaluations gⱼ(k) for 1 ≤ k < degⱼ(g). Update the claim to gⱼ₊₁ for the input value as rⱼ + varsNum() int // number of variables + claimsNum() int // number of claims + proveFinalEval(r []fr.Element) []fr.Element // in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +// sumcheckLazyClaims is the sumcheckClaims data structure on the verifier side. It is "lazy" in that it has to compute fewer things. +type sumcheckLazyClaims interface { + claimsNum() int // claimsNum = m + varsNum() int // varsNum = n + combinedSum(a fr.Element) fr.Element // combinedSum returns c = ∑_{1≤j≤m} aʲ⁻¹cⱼ + degree(i int) int // degree of the total claim in the i'th variable + verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error +} + +// sumcheckProof of a multi-statement. +type sumcheckProof struct { + partialSumPolys []polynomial.Polynomial + finalEvalProof []fr.Element //in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +func setupTranscript(claimsNum int, varsNum int, settings *fiatshamir.Settings) (challengeNames []string, err error) { + numChallenges := varsNum + if claimsNum >= 2 { + numChallenges++ + } + challengeNames = make([]string, numChallenges) + if claimsNum >= 2 { + challengeNames[0] = settings.Prefix + "comb" + } + prefix := settings.Prefix + "pSP." + for i := 0; i < varsNum; i++ { + challengeNames[i+numChallenges-varsNum] = prefix + strconv.Itoa(i) + } + if settings.Transcript == nil { + transcript := fiatshamir.NewTranscript(settings.Hash, challengeNames...) + settings.Transcript = transcript + } + + for i := range settings.BaseChallenges { + if err = settings.Transcript.Bind(challengeNames[0], settings.BaseChallenges[i]); err != nil { + return + } + } + return +} + +func next(transcript *fiatshamir.Transcript, bindings []fr.Element, remainingChallengeNames *[]string) (fr.Element, error) { + challengeName := (*remainingChallengeNames)[0] + for i := range bindings { + bytes := bindings[i].Bytes() + if err := transcript.Bind(challengeName, bytes[:]); err != nil { + return fr.Element{}, err + } + } + var res fr.Element + bytes, err := transcript.ComputeChallenge(challengeName) + res.SetBytes(bytes) + + *remainingChallengeNames = (*remainingChallengeNames)[1:] + + return res, err +} + +// sumcheckProve create a non-interactive proof +func sumcheckProve(claims sumcheckClaims, transcriptSettings fiatshamir.Settings) (sumcheckProof, error) { + + var proof sumcheckProof + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return proof, err + } + + var combinationCoeff fr.Element + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return proof, err + } + } + + varsNum := claims.varsNum() + proof.partialSumPolys = make([]polynomial.Polynomial, varsNum) + proof.partialSumPolys[0] = claims.combine(combinationCoeff) + challenges := make([]fr.Element, varsNum) + + for j := 0; j+1 < varsNum; j++ { + if challenges[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return proof, err + } + proof.partialSumPolys[j+1] = claims.next(challenges[j]) + } + + if challenges[varsNum-1], err = next(transcript, proof.partialSumPolys[varsNum-1], &remainingChallengeNames); err != nil { + return proof, err + } + + proof.finalEvalProof = claims.proveFinalEval(challenges) + + return proof, nil +} + +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, transcriptSettings fiatshamir.Settings) error { + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return err + } + + var combinationCoeff fr.Element + + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return err + } + } + + r := make([]fr.Element, claims.varsNum()) + + // Just so that there is enough room for gJ to be reused + maxDegree := claims.degree(0) + for j := 1; j < claims.varsNum(); j++ { + if d := claims.degree(j); d > maxDegree { + maxDegree = d + } + } + gJ := make(polynomial.Polynomial, maxDegree+1) //At the end of iteration j, gJ = ∑_{i < 2ⁿ⁻ʲ⁻¹} g(X₁, ..., Xⱼ₊₁, i...) NOTE: n is shorthand for claims.varsNum() + gJR := claims.combinedSum(combinationCoeff) // At the beginning of iteration j, gJR = ∑_{i < 2ⁿ⁻ʲ} g(r₁, ..., rⱼ, i...) + + for j := range claims.varsNum() { + if len(proof.partialSumPolys[j]) != claims.degree(j) { + return errors.New("malformed proof") + } + copy(gJ[1:], proof.partialSumPolys[j]) + gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) // Requirement that gⱼ(0) + gⱼ(1) = gⱼ₋₁(r) + // gJ is ready + + //Prepare for the next iteration + if r[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return err + } + // This is an extremely inefficient way of interpolating. TODO: Interpolate without symbolically computing a polynomial + gJCoeffs := polynomial.InterpolateOnRange(gJ[:(claims.degree(j) + 1)]) + gJR = gJCoeffs.Eval(&r[j]) + } + + return claims.verifyFinalEval(r, combinationCoeff, gJR, proof.finalEvalProof) +} diff --git a/internal/gkr/bls24-317/sumcheck_test.go b/internal/gkr/bls24-317/sumcheck_test.go new file mode 100644 index 00000000..ef9210dc --- /dev/null +++ b/internal/gkr/bls24-317/sumcheck_test.go @@ -0,0 +1,152 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/stretchr/testify/assert" + + "math/bits" + + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" + + "strings" + "testing" +) + +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { + poly := make(polynomial.MultiLin, len(polyInt)) + for i, n := range polyInt { + poly[i].SetUint64(n) + } + + claim := singleMultilinClaim{g: poly.Clone()} + + proof, err := sumcheckProve(&claim, fiatshamir.WithHash(hashGenerator())) + if err != nil { + return err + } + + var sb strings.Builder + for _, p := range proof.partialSumPolys { + + sb.WriteString("\t{") + for i := 0; i < len(p); i++ { + sb.WriteString(p[i].String()) + if i+1 < len(p) { + sb.WriteString(", ") + } + } + sb.WriteString("}\n") + } + + lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if err = sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())); err != nil { + return err + } + + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())) == nil { + return fmt.Errorf("bad proof accepted") + } + return nil +} + +func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { + + polys := [][]uint64{ + {1, 2, 3, 4}, // 1 + 2X₁ + X₂ + {1, 2, 3, 4, 5, 6, 7, 8}, // 1 + 4X₁ + 2X₂ + X₃ + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, // 1 + 8X₁ + 4X₂ + 2X₃ + X₄ + } + + const MaxStep = 4 + const MaxStart = 4 + hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + + for step := 0; step < MaxStep; step++ { + for startState := 0; startState < MaxStart; startState++ { + if step == 0 && startState == 1 { // unlucky case where a bad proof would be accepted + continue + } + hashGens = append(hashGens, newMessageCounterGenerator(startState, step)) + } + } + + for _, poly := range polys { + for _, hashGen := range hashGens { + assert.NoError(t, testSumcheckSingleClaimMultilin(poly, hashGen), + "failed with poly %v and hashGen %v", poly, hashGen()) + } + } +} + +type singleMultilinClaim struct { + g polynomial.MultiLin +} + +func (c singleMultilinClaim) proveFinalEval(r []fr.Element) []fr.Element { + return nil // verifier can compute the final eval itself +} + +func (c singleMultilinClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} + +func (c singleMultilinClaim) claimsNum() int { + return 1 +} + +func sumForX1One(g polynomial.MultiLin) polynomial.Polynomial { + sum := g[len(g)/2] + for i := len(g)/2 + 1; i < len(g); i++ { + sum.Add(&sum, &g[i]) + } + return []fr.Element{sum} +} + +func (c singleMultilinClaim) combine(fr.Element) polynomial.Polynomial { + return sumForX1One(c.g) +} + +func (c *singleMultilinClaim) next(r fr.Element) polynomial.Polynomial { + c.g.Fold(r) + return sumForX1One(c.g) +} + +type singleMultilinLazyClaim struct { + g polynomial.MultiLin + claimedSum fr.Element +} + +func (c singleMultilinLazyClaim) verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error { + val := c.g.Evaluate(r, nil) + if val.Equal(&purportedValue) { + return nil + } + return fmt.Errorf("mismatch") +} + +func (c singleMultilinLazyClaim) combinedSum(combinationCoeffs fr.Element) fr.Element { + return c.claimedSum +} + +func (c singleMultilinLazyClaim) degree(i int) int { + return 1 +} + +func (c singleMultilinLazyClaim) claimsNum() int { + return 1 +} + +func (c singleMultilinLazyClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} diff --git a/internal/gkr/bls24-317/test_vector_utils_test.go b/internal/gkr/bls24-317/test_vector_utils_test.go new file mode 100644 index 00000000..c740f305 --- /dev/null +++ b/internal/gkr/bls24-317/test_vector_utils_test.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "strings" + + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" + "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrtesting" +) + +func toElement(i int64) *fr.Element { + var res fr.Element + res.SetInt64(i) + return &res +} + +func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { + if _type, ok := d["type"]; ok { + switch _type { + case "const": + startState := int64(d["val"].(float64)) + return &messageCounter{startState: startState, step: 0, state: startState}, nil + default: + return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) + } + } + return nil, fmt.Errorf("hash description missing type") +} + +type messageCounter struct { + startState int64 + state int64 + step int64 +} + +func (m *messageCounter) Write(p []byte) (n int, err error) { + inputBlockSize := (len(p)-1)/fr.Bytes + 1 + m.state += int64(inputBlockSize) * m.step + return len(p), nil +} + +func (m *messageCounter) Sum(b []byte) []byte { + inputBlockSize := (len(b)-1)/fr.Bytes + 1 + resI := m.state + int64(inputBlockSize)*m.step + var res fr.Element + res.SetInt64(int64(resI)) + resBytes := res.Bytes() + return resBytes[:] +} + +func (m *messageCounter) Reset() { + m.state = m.startState +} + +func (m *messageCounter) Size() int { + return fr.Bytes +} + +func (m *messageCounter) BlockSize() int { + return fr.Bytes +} + +func newMessageCounter(startState, step int) hash.Hash { + transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} + return transcript +} + +func newMessageCounterGenerator(startState, step int) func() hash.Hash { + return func() hash.Hash { + return newMessageCounter(startState, step) + } +} + +func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { + + // TODO: Put this in element.SetString? + switch v := value.(type) { + case string: + + if sep := strings.Split(v, "/"); len(sep) == 2 { + var denom fr.Element + if _, err := z.SetString(sep[0]); err != nil { + return nil, err + } + if _, err := denom.SetString(sep[1]); err != nil { + return nil, err + } + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil + } + + case float64: + asInt := int64(v) + if float64(asInt) != v { + return nil, fmt.Errorf("cannot currently parse float") + } + z.SetInt64(asInt) + return z, nil + } + + return z.SetInterface(value) +} + +func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { + elementSlice := make([]fr.Element, len(slice)) + for i, v := range slice { + if _, err := setElement(&elementSlice[i], v); err != nil { + return nil, err + } + } + return elementSlice, nil +} + +func sliceEquals(a []fr.Element, b []fr.Element) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if !a[i].Equal(&b[i]) { + return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) + } + } + return nil +} + +func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if err := sliceEquals(a[i], b[i]); err != nil { + return fmt.Errorf("at index %d: %w", i, err) + } + } + return nil +} diff --git a/internal/gkr/bn254/gate_testing.go b/internal/gkr/bn254/gate_testing.go new file mode 100644 index 00000000..716ba389 --- /dev/null +++ b/internal/gkr/bn254/gate_testing.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + + "slices" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// IsGateFunctionAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func IsGateFunctionAdditive(f gkr.GateFunction, i, nbIn int) bool { + fWrapped := api.convertFunc(f) + + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 := fWrapped(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := fWrapped(in...) + + y2.Sub(y2, y1) + y1.Sub(y1, y0) + + if !y2.Equal(y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 = fWrapped(in...) + + y1.Sub(y1, y0) + + return y1.Equal(y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f gateFunctionFr) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i].Set(f(fIn...)) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +// FindGateFunctionDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func FindGateFunctionDegree(f gkr.GateFunction, max, nbIn int) (int, error) { + fFr := api.convertFunc(f) + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := fFr.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, gkrtypes.ErrZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func VerifyGateFunctionDegree(f gkr.GateFunction, claimedDegree, nbIn int) error { + fFr := api.convertFunc(f) + if p := fFr.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return gkrtypes.ErrZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} diff --git a/internal/gkr/bn254/gkr.go b/internal/gkr/bn254/gkr.go new file mode 100644 index 00000000..04cf3512 --- /dev/null +++ b/internal/gkr/bn254/gkr.go @@ -0,0 +1,816 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "sync" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// The goal is to prove/verify evaluations of many instances of the same circuit + +// WireAssignment is assignment of values to the same wire across many instances of the circuit +type WireAssignment []polynomial.MultiLin + +type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) + +// eqTimesGateEvalSumcheckLazyClaims is a lazy claim for sumcheck (verifier side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the checking of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckLazyClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ), allegedly + manager *claimsManager // WARNING: Circular references +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) getWire() *gkrtypes.Wire { + return e.manager.wires[e.wireI] +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) claimsNum() int { + return len(e.evaluationPoints) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) varsNum() int { + return len(e.evaluationPoints[0]) +} + +// combinedSum returns ∑ᵢ aⁱ yᵢ +func (e *eqTimesGateEvalSumcheckLazyClaims) combinedSum(a fr.Element) fr.Element { + evalsAsPoly := polynomial.Polynomial(e.claimedEvaluations) + return evalsAsPoly.Eval(&a) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) degree(int) int { + return 1 + e.manager.wires[e.wireI].Gate.Degree() +} + +// verifyFinalEval finalizes the verification of w. +// The prover's claims w(xᵢ) = yᵢ have already been reduced to verifying +// ∑ cⁱ eq(xᵢ, r) w(r) = purportedValue. ( c is combinationCoeff ) +// Both purportedValue and the vector r have been randomized during the sumcheck protocol. +// By taking the w term out of the sum we get the equivalent claim that +// for E := ∑ eq(xᵢ, r), it must be that E w(r) = purportedValue. +// If w is an input wire, the verifier can directly check its evaluation at r. +// Otherwise, the prover makes claims about the evaluation of w's input wires, +// wᵢ, at r, to be verified later. +// The claims are communicated through the proof parameter. +// The verifier checks here if the claimed evaluations of wᵢ(r) are consistent with +// the main claim, by checking E w(wᵢ(r)...) = purportedValue. +func (e *eqTimesGateEvalSumcheckLazyClaims) verifyFinalEval(r []fr.Element, combinationCoeff, purportedValue fr.Element, uniqueInputEvaluations []fr.Element) error { + // the eq terms ( E ) + numClaims := len(e.evaluationPoints) + evaluation := polynomial.EvalEq(e.evaluationPoints[numClaims-1], r) + for i := numClaims - 2; i >= 0; i-- { + evaluation.Mul(&evaluation, &combinationCoeff) + eq := polynomial.EvalEq(e.evaluationPoints[i], r) + evaluation.Add(&evaluation, &eq) + } + + wire := e.manager.wires[e.wireI] + + // the w(...) term + var gateEvaluation fr.Element + if wire.IsInput() { // just compute w(r) + gateEvaluation = e.manager.assignment[e.wireI].Evaluate(r, e.manager.memPool) + } else { // proof contains the evaluations of the inputs, but avoids repetition in case multiple inputs come from the same wire + injection, injectionLeftInv := + e.manager.wires.ClaimPropagationInfo(e.wireI) + + if len(injection) != len(uniqueInputEvaluations) { + return fmt.Errorf("%d input wire evaluations given, %d expected", len(uniqueInputEvaluations), len(injection)) + } + + for uniqueI, i := range injection { // map from unique to all + e.manager.add(wire.Inputs[i], r, uniqueInputEvaluations[uniqueI]) + } + + inputEvaluations := make([]frontend.Variable, len(wire.Inputs)) + for i, uniqueI := range injectionLeftInv { // map from all to unique + inputEvaluations[i] = &uniqueInputEvaluations[uniqueI] + } + + gateEvaluation.Set(wire.Gate.Evaluate(api, inputEvaluations...).(*fr.Element)) + } + + evaluation.Mul(&evaluation, &gateEvaluation) + + if evaluation.Equal(&purportedValue) { + return nil + } + return errors.New("incompatible evaluations") +} + +// eqTimesGateEvalSumcheckClaims is a claim for sumcheck (prover side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the proving of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ) + manager *claimsManager + + input []polynomial.MultiLin // input[i](h₁, ..., hₘ₋ⱼ) = wᵢ(r₁, r₂, ..., rⱼ₋₁, h₁, ..., hₘ₋ⱼ) + + eq polynomial.MultiLin // E := ∑ᵢ cⁱ eq(xᵢ, -) +} + +func (c *eqTimesGateEvalSumcheckClaims) getWire() *gkrtypes.Wire { + return c.manager.wires[c.wireI] +} + +// combine the multiple claims into one claim using a random combination (combinationCoeff or c). +// From the original multiple claims of w(xᵢ) = yᵢ, we get a single claim +// ∑ᵢ,ₕ cⁱ eq(xᵢ, h) w(h) = ∑ᵢ cⁱ yᵢ, where h iterates over the hypercube (circuit instances) and +// i iterates over the claims. +// Equivalently, we could say ∑ᵢ cⁱ yᵢ = ∑ₕ,ᵢ cⁱ eq(xᵢ, h) w(h) = ∑ₕ w(h) ∑ᵢ cⁱ eq(xᵢ, h). +// Thus if we initially compute E := ∑ᵢ cⁱ eq(xᵢ, -), our claim will find the simpler form +// ∑ᵢ cⁱ yᵢ = ∑ₕ w(h) E(h), where the sum-checked polynomial is of degree deg(g) + 1, +// and deg(g) is the total degree of the polynomial defining the gate g of which w is the output. +// The output of combine is the first sumcheck claim, i.e. ∑₍ₕ₁,ₕ₂,...₎ w(X, h₁, h₂, ...) E(X, h₁, h₂, ...).. +func (c *eqTimesGateEvalSumcheckClaims) combine(combinationCoeff fr.Element) polynomial.Polynomial { + varsNum := c.varsNum() + eqLength := 1 << varsNum + claimsNum := c.claimsNum() + // initialize the eq tables ( E ) + c.eq = c.manager.memPool.Make(eqLength) + + c.eq[0].SetOne() + c.eq.Eq(c.evaluationPoints[0]) + + // E := eq(x₀, -) + newEq := polynomial.MultiLin(c.manager.memPool.Make(eqLength)) + aI := combinationCoeff + + // E += cⁱ eq(xᵢ, -) + for k := 1; k < claimsNum; k++ { + newEq[0].Set(&aI) + + c.eqAcc(c.eq, newEq, c.evaluationPoints[k]) + + if k+1 < claimsNum { + aI.Mul(&aI, &combinationCoeff) + } + } + + c.manager.memPool.Dump(newEq) + + return c.computeGJ() +} + +// eqAcc sets m to an eq table at q and then adds it to e. +// m <- eq(q, -). +// e <- e + m +func (c *eqTimesGateEvalSumcheckClaims) eqAcc(e, m polynomial.MultiLin, q []fr.Element) { + n := len(q) + + //At the end of each iteration, m(h₁, ..., hₙ) = eq(q₁, ..., qᵢ₊₁, h₁, ..., hᵢ₊₁) + for i := range q { // In the comments we use a 1-based index so q[i] = qᵢ₊₁ + // go through all assignments of (b₁, ..., bᵢ) ∈ {0,1}ⁱ + const threshold = 1 << 6 + k := 1 << i + if k < threshold { + for j := 0; j < k; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + } else { + c.manager.workers.Submit(k, func(start, end int) { + for j := start; j < end; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + }, 1024).Wait() + } + + } + c.manager.workers.Submit(len(e), func(start, end int) { + for i := start; i < end; i++ { + e[i].Add(&e[i], &m[i]) + } + }, 512).Wait() +} + +// computeGJ: gⱼ = ∑_{0≤h<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, h...) = ∑_{0≤i<2ⁿ⁻ʲ} E(r₁, ..., Xⱼ, h...) g( w₀(r₁, ..., Xⱼ, h...), ... ). +// the polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). +// The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). By convention, g₀ is a constant polynomial equal to the claimed sum. +func (c *eqTimesGateEvalSumcheckClaims) computeGJ() polynomial.Polynomial { + + wire := c.getWire() + degGJ := 1 + wire.Gate.Degree() // guaranteed to be no smaller than the actual deg(gⱼ) + nbGateIn := len(c.input) + + // Both E and wᵢ (the input wires and the eq table) are multilinear, thus + // they are linear in Xⱼ. + // So for f ∈ { E(r₁, ..., Xⱼ, h...) } ∪ {wᵢ(r₁, ..., Xⱼ, h...) }, so f(m) = m×(f(1) - f(0)) + f(0), and f(0), f(1) are easily computed from the bookkeeping tables. + // ml are such multilinear polynomials the evaluations of which over different values of Xⱼ are computed in this stepwise manner. + ml := make([]polynomial.MultiLin, nbGateIn+1) // shortcut to the evaluations of the multilinear polynomials over the hypercube + ml[0] = c.eq + copy(ml[1:], c.input) + + sumSize := len(c.eq) / 2 // the range of h, over which we sum + + // Perf-TODO: Collate once at claim "combination" time and not again. then, even folding can be done in one operation every time "next" is called + + gJ := make([]fr.Element, degGJ) + var mu sync.Mutex + computeAll := func(start, end int) { // compute method to allow parallelization across instances + var step fr.Element + + res := make([]fr.Element, degGJ) + + // evaluations of ml, laid out as: + // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), + // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), + // ... + // ml[0](degGJ, h...), ml[2](degGJ, h...), ..., ml[len(ml)-1](degGJ, h...) + mlEvals := make([]fr.Element, degGJ*len(ml)) + gateInput := make([]frontend.Variable, nbGateIn) + + for h := start; h < end; h++ { // h counts across instances + + evalAt1Index := sumSize + h + for k := range ml { + // d = 0 + mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1. Can be taken directly from the table. + step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for d := 1; d < degGJ; d++ { + mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + } + } + + eIndex := 0 // index for where the current eq term is + nextEIndex := len(ml) + for d := range degGJ { + for i := range gateInput { + gateInput[i] = &mlEvals[eIndex+1+i] + } + summand := wire.Gate.Evaluate(api, gateInput...).(*fr.Element) + summand.Mul(summand, &mlEvals[eIndex]) + res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + } + } + mu.Lock() + for i := range gJ { + gJ[i].Add(&gJ[i], &res[i]) // collect into the complete sum + } + mu.Unlock() + } + + const minBlockSize = 64 + + if sumSize < minBlockSize { + // no parallelization + computeAll(0, sumSize) + } else { + c.manager.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } + + return gJ +} + +// next first folds the input and E polynomials at the given verifier challenge then computes the new gⱼ. +// Thus, j <- j+1 and rⱼ = challenge. +func (c *eqTimesGateEvalSumcheckClaims) next(challenge fr.Element) polynomial.Polynomial { + const minBlockSize = 512 + n := len(c.eq) / 2 + if n < minBlockSize { + // no parallelization + for i := 0; i < len(c.input); i++ { + c.input[i].Fold(challenge) + } + c.eq.Fold(challenge) + } else { + wgs := make([]*sync.WaitGroup, len(c.input)) + for i := 0; i < len(c.input); i++ { + wgs[i] = c.manager.workers.Submit(n, c.input[i].FoldParallel(challenge), minBlockSize) + } + c.manager.workers.Submit(n, c.eq.FoldParallel(challenge), minBlockSize).Wait() + for _, wg := range wgs { + wg.Wait() + } + } + + return c.computeGJ() +} + +func (c *eqTimesGateEvalSumcheckClaims) varsNum() int { + return len(c.evaluationPoints[0]) +} + +func (c *eqTimesGateEvalSumcheckClaims) claimsNum() int { + return len(c.claimedEvaluations) +} + +// proveFinalEval provides the values wᵢ(r₁, ..., rₙ) +func (c *eqTimesGateEvalSumcheckClaims) proveFinalEval(r []fr.Element) []fr.Element { + //defer the proof, return list of claims + + injection, _ := c.manager.wires.ClaimPropagationInfo(c.wireI) // TODO @Tabaie: Instead of doing this last, we could just have fewer input in the first place; not that likely to happen with single gates, but more so with layers. + evaluations := make([]fr.Element, len(injection)) + for i, gateInputI := range injection { + wI := c.input[gateInputI] + wI.Fold(r[len(r)-1]) // We already have wᵢ(r₁, ..., rₙ₋₁, hₙ) in a table. Only one more fold required. + c.manager.add(c.getWire().Inputs[gateInputI], r, wI[0]) + evaluations[i] = wI[0] + } + + c.manager.memPool.Dump(c.claimedEvaluations, c.eq) + + return evaluations +} + +type claimsManager struct { + claims []*eqTimesGateEvalSumcheckLazyClaims + assignment WireAssignment + memPool *polynomial.Pool + workers *utils.WorkerPool + wires gkrtypes.Wires +} + +func newClaimsManager(wires []*gkrtypes.Wire, assignment WireAssignment, o settings) (manager claimsManager) { + manager.assignment = assignment + manager.claims = make([]*eqTimesGateEvalSumcheckLazyClaims, len(wires)) + manager.memPool = o.pool + manager.workers = o.workers + manager.wires = wires + + for i, wire := range wires { + + manager.claims[i] = &eqTimesGateEvalSumcheckLazyClaims{ + wireI: i, + evaluationPoints: make([][]fr.Element, 0, wire.NbClaims()), + claimedEvaluations: manager.memPool.Make(wire.NbClaims()), + manager: &manager, + } + } + return +} + +func (m *claimsManager) add(wire int, evaluationPoint []fr.Element, evaluation fr.Element) { + claim := m.claims[wire] + i := len(claim.evaluationPoints) + claim.claimedEvaluations[i] = evaluation + claim.evaluationPoints = append(claim.evaluationPoints, evaluationPoint) +} + +func (m *claimsManager) getLazyClaim(wire int) *eqTimesGateEvalSumcheckLazyClaims { + return m.claims[wire] +} + +func (m *claimsManager) getClaim(wireI int) *eqTimesGateEvalSumcheckClaims { + lazy := m.claims[wireI] + wire := m.wires[wireI] + res := &eqTimesGateEvalSumcheckClaims{ + wireI: wireI, + evaluationPoints: lazy.evaluationPoints, + claimedEvaluations: lazy.claimedEvaluations, + manager: m, + } + + if wire.IsInput() { + res.input = []polynomial.MultiLin{m.memPool.Clone(m.assignment[wireI])} + } else { + res.input = make([]polynomial.MultiLin, len(wire.Inputs)) + + for inputI, inputW := range wire.Inputs { + res.input[inputI] = m.memPool.Clone(m.assignment[inputW]) //will be edited later, so must be deep copied + } + } + return res +} + +func (m *claimsManager) deleteClaim(wire int) { + m.claims[wire].manager = nil + m.claims[wire] = nil +} + +type settings struct { + pool *polynomial.Pool + sorted []*gkrtypes.Wire + transcript *fiatshamir.Transcript + transcriptPrefix string + nbVars int + workers *utils.WorkerPool +} + +type Option func(*settings) + +func WithPool(pool *polynomial.Pool) Option { + return func(options *settings) { + options.pool = pool + } +} + +func WithSortedCircuit(sorted []*gkrtypes.Wire) Option { + return func(options *settings) { + options.sorted = sorted + } +} + +func WithWorkers(workers *utils.WorkerPool) Option { + return func(options *settings) { + options.workers = workers + } +} + +func setup(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (settings, error) { + var o settings + var err error + for _, option := range options { + option(&o) + } + + o.nbVars = assignment.NumVars() + nbInstances := assignment.NumInstances() + if 1< 1 { //combine the claims + size++ + } + size += logNbInstances // full run of sumcheck on logNbInstances variables + } + + nums := make([]string, max(len(sorted), logNbInstances)) + for i := range nums { + nums[i] = strconv.Itoa(i) + } + + challenges := make([]string, size) + + // output wire claims + firstChallengePrefix := prefix + "fC." + for j := 0; j < logNbInstances; j++ { + challenges[j] = firstChallengePrefix + nums[j] + } + j := logNbInstances + for i := len(sorted) - 1; i >= 0; i-- { + if sorted[i].NoProof() { + continue + } + wirePrefix := prefix + "w" + nums[i] + "." + + if sorted[i].NbClaims() > 1 { + challenges[j] = wirePrefix + "comb" + j++ + } + + partialSumPrefix := wirePrefix + "pSP." + for k := 0; k < logNbInstances; k++ { + challenges[j] = partialSumPrefix + nums[k] + j++ + } + } + return challenges +} + +func getFirstChallengeNames(logNbInstances int, prefix string) []string { + res := make([]string, logNbInstances) + firstChallengePrefix := prefix + "fC." + for i := 0; i < logNbInstances; i++ { + res[i] = firstChallengePrefix + strconv.Itoa(i) + } + return res +} + +func getChallenges(transcript *fiatshamir.Transcript, names []string) ([]fr.Element, error) { + res := make([]fr.Element, len(names)) + for i, name := range names { + if bytes, err := transcript.ComputeChallenge(name); err == nil { + res[i].SetBytes(bytes) + } else { + return nil, err + } + } + return res, nil +} + +// Prove consistency of the claimed assignment +func Prove(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (Proof, error) { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return nil, err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + proof := make(Proof, len(c)) + // firstChallenge called rho in the paper + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return nil, err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + claim := claims.getClaim(i) + if wire.NoProof() { // input wires with one claim only + proof[i] = sumcheckProof{ + partialSumPolys: []polynomial.Polynomial{}, + finalEvalProof: []fr.Element{}, + } + } else { + if proof[i], err = sumcheckProve( + claim, fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err != nil { + return proof, err + } + + baseChallenge = make([][]byte, len(proof[i].finalEvalProof)) + for j := range proof[i].finalEvalProof { + baseChallenge[j] = proof[i].finalEvalProof[j].Marshal() + } + } + // the verifier checks a single claim about input wires itself + claims.deleteClaim(i) + } + + return proof, nil +} + +// Verify the consistency of the claimed output with the claimed input +// Unlike in Prove, the assignment argument need not be complete +func Verify(c gkrtypes.Circuit, assignment WireAssignment, proof Proof, transcriptSettings fiatshamir.Settings, options ...Option) error { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + proofW := proof[i] + claim := claims.getLazyClaim(i) + if wire.NoProof() { // input wires with one claim only + // make sure the proof is empty + if len(proofW.finalEvalProof) != 0 || len(proofW.partialSumPolys) != 0 { + return errors.New("no proof allowed for input wire with a single claim") + } + + if wire.NbClaims() == 1 { // input wire + // simply evaluate and see if it matches + evaluation := assignment[i].Evaluate(claim.evaluationPoints[0], claims.memPool) + if !claim.claimedEvaluations[0].Equal(&evaluation) { + return errors.New("incorrect input wire claim") + } + } + } else if err = sumcheckVerify( + claim, proof[i], fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err == nil { // incorporate prover claims about w's input into the transcript + baseChallenge = make([][]byte, len(proofW.finalEvalProof)) + for j := range baseChallenge { + baseChallenge[j] = proofW.finalEvalProof[j].Marshal() + } + } else { + return fmt.Errorf("sumcheck proof rejected: %v", err) //TODO: Any polynomials to dump? + } + claims.deleteClaim(i) + } + return nil +} + +// Complete the circuit evaluation from input values +func (a WireAssignment) Complete(wires gkrtypes.Wires) WireAssignment { + + nbInstances := a.NumInstances() + maxNbIns := 0 + + for i, w := range wires { + maxNbIns = max(maxNbIns, len(w.Inputs)) + if len(a[i]) != nbInstances { + a[i] = make([]fr.Element, nbInstances) + } + } + + ins := make([]fr.Element, maxNbIns) + for i := range nbInstances { + for wI, w := range wires { + if !w.IsInput() { + for inI, in := range w.Inputs { + ins[inI] = a[in][i] + } + a[wI][i].Set(api.evaluate(w.Gate.Evaluate, ins[:len(w.Inputs)]...)) + } + } + } + + return a +} + +func (a WireAssignment) NumInstances() int { + for _, aW := range a { + return len(aW) + } + panic("empty assignment") +} + +func (a WireAssignment) NumVars() int { + for _, aW := range a { + return aW.NumVars() + } + panic("empty assignment") +} + +// SerializeToBigInts flattens a proof object into the given slice of big.Ints +// useful in gnark hints. +func (p Proof) SerializeToBigInts(outs []*big.Int) error { + offset := 0 + for i := range p { + for _, poly := range p[i].partialSumPolys { + frToBigInts(outs[offset:], poly) + offset += len(poly) + } + if p[i].finalEvalProof != nil { + frToBigInts(outs[offset:], p[i].finalEvalProof) + offset += len(p[i].finalEvalProof) + } + } + if offset != len(outs) { + return fmt.Errorf("expected %d elements, got %d", offset, len(outs)) + } + return nil +} + +func frToBigInts(dst []*big.Int, src []fr.Element) { + for i := range src { + src[i].BigInt(dst[i]) + } +} + +// gateAPI implements gkr.GateAPI. +type gateAPI struct{} + +var api gateAPI + +func (gateAPI) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element // TODO Heap allocated. Keep an eye on perf + res.Add(cast(i1), cast(i2)) + for _, v := range in { + res.Add(&res, cast(v)) + } + return &res +} + +func (gateAPI) MulAcc(a, b, c frontend.Variable) frontend.Variable { + var prod fr.Element + prod.Add(cast(b), cast(c)) + res := cast(a) + res.Add(res, &prod) + return &res +} + +func (gateAPI) Neg(i1 frontend.Variable) frontend.Variable { + var res fr.Element + res.Neg(cast(i1)) + return &res +} + +func (gateAPI) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Sub(cast(i1), cast(i2)) + for _, v := range in { + res.Sub(&res, cast(v)) + } + return &res +} + +func (gateAPI) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Mul(cast(i1), cast(i2)) + for _, v := range in { + res.Mul(&res, cast(v)) + } + return &res +} + +func (gateAPI) Println(a ...frontend.Variable) { + toPrint := make([]any, len(a)) + var x fr.Element + + for i, v := range a { + if _, err := x.SetInterface(v); err != nil { + toPrint[i] = x.String() + } else { + if s, ok := v.(string); ok { + toPrint[i] = s + continue + } + panic(fmt.Errorf("not numeric or string: %w", err)) + } + } + fmt.Println(toPrint...) +} + +func (api gateAPI) evaluate(f gkr.GateFunction, in ...fr.Element) *fr.Element { + inVar := make([]frontend.Variable, len(in)) + for i := range in { + inVar[i] = &in[i] + } + return f(api, inVar...).(*fr.Element) +} + +type gateFunctionFr func(...fr.Element) *fr.Element + +// convertFunc turns f into a function that accepts and returns fr.Element. +func (api gateAPI) convertFunc(f gkr.GateFunction) gateFunctionFr { + return func(in ...fr.Element) *fr.Element { + return api.evaluate(f, in...) + } +} + +func cast(v frontend.Variable) *fr.Element { + if x, ok := v.(*fr.Element); ok { // fast path, no extra heap allocation + return x + } + var x fr.Element + if _, err := x.SetInterface(v); err != nil { + panic(err) + } + return &x +} diff --git a/internal/gkr/bn254/gkr_test.go b/internal/gkr/bn254/gkr_test.go new file mode 100644 index 00000000..e03d2aca --- /dev/null +++ b/internal/gkr/bn254/gkr_test.go @@ -0,0 +1,505 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "os" + "path/filepath" + "reflect" + "strconv" + "testing" + "time" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + gcUtils "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/internal/utils" + "github.com/consensys/gnark/std/gkrapi/gkr" + "github.com/stretchr/testify/assert" +) + +func TestNoGateTwoInstances(t *testing.T) { + // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case + testNoGate(t, []fr.Element{four, three}) +} + +func TestNoGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}}) +} + +func TestSingleAddGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Add2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleMulGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Mul2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleInputTwoIdentityGates(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + }) +} + +func TestSingleInputTwoIdentityGatesComposed(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{1}, + }}) +} + +func TestAPowNTimesBCircuit(t *testing.T) { + const N = 10 + + c := make(gkrtypes.Circuit, N+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: gkrtypes.Mul2(), + Inputs: []int{i - 1, 0}, + } + } + + test(t, c) +} + +func TestSingleMimcCipherGate(t *testing.T) { + test(t, gkrtypes.Circuit{ + {}, {}, + { + Inputs: []int{0, 1}, + Gate: cache.GetGate("mimc"), + }, + }) +} + +func TestShallowMimcTwoInstances(t *testing.T) { + test(t, mimcCircuit(2)) +} + +func TestMimc(t *testing.T) { + test(t, mimcCircuit(93)) +} + +func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { + circuit := gkrtypes.Circuit{gkrtypes.Wire{ + Gate: gkrtypes.Identity(), + NbUniqueOutputs: 2, + }} + + assignment := WireAssignment{[]fr.Element{two, three}} + var o settings + pool := polynomial.NewPool(256, 1<<11) + workers := gcUtils.NewWorkerPool() + o.pool = &pool + o.workers = workers + + claimsManagerGen := func() *claimsManager { + manager := newClaimsManager(utils.References(circuit), assignment, o) + manager.add(0, []fr.Element{three}, five) + manager.add(0, []fr.Element{four}, six) + return &manager + } + + transcriptGen := newMessageCounterGenerator(4, 1) + + proof, err := sumcheckProve(claimsManagerGen().getClaim(0), fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) + err = sumcheckVerify(claimsManagerGen().getLazyClaim(0), proof, fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) +} + +var one, two, three, four, five, six fr.Element + +func init() { + one.SetOne() + two.Double(&one) + three.Add(&two, &one) + four.Double(&two) + five.Add(&three, &two) + six.Double(&three) +} + +var testManyInstancesLogMaxInstances = -1 + +func getLogMaxInstances(t *testing.T) int { + if testManyInstancesLogMaxInstances == -1 { + + s := os.Getenv("GKR_LOG_INSTANCES") + if s == "" { + testManyInstancesLogMaxInstances = 5 + } else { + var err error + testManyInstancesLogMaxInstances, err = strconv.Atoi(s) + if err != nil { + t.Error(err) + } + } + + } + return testManyInstancesLogMaxInstances +} + +func test(t *testing.T, circuit gkrtypes.Circuit) { + wireRefs := utils.References(circuit) + ins := circuit.Inputs() + insAssignment := make(WireAssignment, len(ins)) + maxSize := 1 << getLogMaxInstances(t) + + for i := range ins { + insAssignment[i] = make([]fr.Element, maxSize) + fr.Vector(insAssignment[i]).MustSetRandom() + } + + fullAssignment := make(WireAssignment, len(circuit)) + for _, numEvals := range []int{2, maxSize} { + for i := range ins { + fullAssignment[ins[i]] = insAssignment[i][:numEvals] + } + + fullAssignment.Complete(wireRefs) + + t.Log("Selected inputs for test") + + proof, err := Prove(circuit, fullAssignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") + + if proof.isEmpty() { // special case for TestNoGate: + continue // there's no way to make a trivial proof fail + } + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(0, 1))) + assert.NotNil(t, err, "bad proof accepted") + } + +} + +func (p Proof) isEmpty() bool { + for i := range p { + if len(p[i].finalEvalProof) != 0 { + return false + } + for j := range p[i].partialSumPolys { + if len(p[i].partialSumPolys[j]) != 0 { + return false + } + } + } + return true +} + +func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { + c := gkrtypes.Circuit{ + {}, + } + + assignment := WireAssignment{0: inputAssignments[0]} + + proof, err := Prove(c, assignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(c, assignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") +} + +func mimcCircuit(numRounds int) gkrtypes.Circuit { + c := make(gkrtypes.Circuit, numRounds+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: cache.GetGate("mimc"), + Inputs: []int{i - 1, 0}, + } + } + return c +} + +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("bivariate input needed") + } + res := api.Add(x[0], x[1]) + return api.Mul(res, x[0]) + } + + // g: x,y -> x² + 3y + g := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + res := api.Mul(x[0], x[0]) + y3 := api.Mul(x[1], 3) + return api.Add(res, y3) + } + + // h: x -> 2x + // but it edits it input + h := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + return api.Add(x[0], x[0]) + } + + assert.False(t, IsGateFunctionAdditive(f, 1, 2)) + assert.False(t, IsGateFunctionAdditive(f, 0, 2)) + + assert.False(t, IsGateFunctionAdditive(g, 0, 2)) + assert.True(t, IsGateFunctionAdditive(g, 1, 2)) + + assert.True(t, IsGateFunctionAdditive(h, 0, 1)) +} + +func generateTestProver(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + proof, err := Prove(testCase.Circuit, testCase.FullAssignment, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err) + assert.NoError(t, proofEquals(testCase.Proof, proof)) + } +} + +func generateTestVerifier(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err, "proof rejected") + testCase, err = newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(newMessageCounter(2, 0))) + assert.NotNil(t, err, "bad proof accepted") + } +} + +func TestGkrVectors(t *testing.T) { + + const testDirPath = "../test_vectors/" + dirEntries, err := os.ReadDir(testDirPath) + assert.NoError(t, err) + for _, dirEntry := range dirEntries { + if !dirEntry.IsDir() { + + if filepath.Ext(dirEntry.Name()) == ".json" { + path := filepath.Join(testDirPath, dirEntry.Name()) + noExt := dirEntry.Name()[:len(dirEntry.Name())-len(".json")] + + t.Run(noExt+"_prover", generateTestProver(path)) + t.Run(noExt+"_verifier", generateTestVerifier(path)) + + } + } + } +} + +func proofEquals(expected Proof, seen Proof) error { + if len(expected) != len(seen) { + return fmt.Errorf("length mismatch %d ≠ %d", len(expected), len(seen)) + } + for i, x := range expected { + xSeen := seen[i] + + if xSeen.finalEvalProof == nil { + if seenFinalEval := x.finalEvalProof; len(seenFinalEval) != 0 { + return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) + } + } else { + if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + return fmt.Errorf("final evaluation proof mismatch") + } + } + if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + return err + } + } + return nil +} + +func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { + fmt.Println("creating circuit structure") + c := mimcCircuit(mimcDepth) + + in0 := make([]fr.Element, nbInstances) + in1 := make([]fr.Element, nbInstances) + fr.Vector(in0).MustSetRandom() + fr.Vector(in1).MustSetRandom() + + fmt.Println("evaluating circuit") + start := time.Now().UnixMicro() + assignment := WireAssignment{in0, in1}.Complete(utils.References(c)) + solved := time.Now().UnixMicro() - start + fmt.Println("solved in", solved, "μs") + + //b.ResetTimer() + fmt.Println("constructing proof") + start = time.Now().UnixMicro() + _, err := Prove(c, assignment, fiatshamir.WithHash(mimc.NewMiMC())) + proved := time.Now().UnixMicro() - start + fmt.Println("proved in", proved, "μs") + assert.NoError(b, err) +} + +func BenchmarkGkrMimc19(b *testing.B) { + benchmarkGkrMiMC(b, 1<<19, 91) +} + +func BenchmarkGkrMimc17(b *testing.B) { + benchmarkGkrMiMC(b, 1<<17, 91) +} + +func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { + proof := make(Proof, len(printable)) + for i := range printable { + finalEvalProof := []fr.Element(nil) + + if printable[i].FinalEvalProof != nil { + finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) + finalEvalProof = make([]fr.Element, finalEvalSlice.Len()) + for k := range finalEvalProof { + if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + return nil, err + } + } + } + + proof[i] = sumcheckProof{ + partialSumPolys: make([]polynomial.Polynomial, len(printable[i].PartialSumPolys)), + finalEvalProof: finalEvalProof, + } + for k := range printable[i].PartialSumPolys { + var err error + if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + return nil, err + } + } + } + return proof, nil +} + +type TestCase struct { + Circuit gkrtypes.Circuit + Hash hash.Hash + Proof Proof + FullAssignment WireAssignment + InOutAssignment WireAssignment +} + +var ( + testCases = make(map[string]*TestCase) + cache = gkrtesting.NewCache() +) + +func newTestCase(path string) (*TestCase, error) { + path, err := filepath.Abs(path) + if err != nil { + return nil, err + } + dir := filepath.Dir(path) + + tCase, ok := testCases[path] + if ok { + return tCase, nil + } + + info, err := cache.ReadTestCaseInfo(path) + if err != nil { + return nil, err + } + + circuit := cache.GetCircuit(filepath.Join(dir, info.Circuit)) + var _hash hash.Hash + if _hash, err = hashFromDescription(info.Hash); err != nil { + return nil, err + } + var proof Proof + if proof, err = unmarshalProof(info.Proof); err != nil { + return nil, err + } + + fullAssignment := make(WireAssignment, len(circuit)) + inOutAssignment := make(WireAssignment, len(circuit)) + + sorted := circuit.TopologicalSort() + + inI, outI := 0, 0 + for i, w := range sorted { + var assignmentRaw []interface{} + if w.IsInput() { + if inI == len(info.Input) { + return nil, fmt.Errorf("fewer input in vector than in circuit") + } + assignmentRaw = info.Input[inI] + inI++ + } else if w.IsOutput() { + if outI == len(info.Output) { + return nil, fmt.Errorf("fewer output in vector than in circuit") + } + assignmentRaw = info.Output[outI] + outI++ + } + if assignmentRaw != nil { + var wireAssignment []fr.Element + if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + return nil, err + } + + fullAssignment[i] = wireAssignment + inOutAssignment[i] = wireAssignment + } + } + + fullAssignment.Complete(utils.References(circuit)) + + for i, w := range sorted { + if w.IsOutput() { + if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + return nil, fmt.Errorf("assignment mismatch: %v", err) + } + } + } + + tCase = &TestCase{ + FullAssignment: fullAssignment, + InOutAssignment: inOutAssignment, + Proof: proof, + Hash: _hash, + Circuit: circuit, + } + + testCases[path] = tCase + + return tCase, nil +} diff --git a/internal/gkr/bn254/solver_hints.go b/internal/gkr/bn254/solver_hints.go new file mode 100644 index 00000000..7bc37829 --- /dev/null +++ b/internal/gkr/bn254/solver_hints.go @@ -0,0 +1,146 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark-crypto/hash" + "github.com/consensys/gnark-crypto/utils" + hint "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + algo_utils "github.com/consensys/gnark/internal/utils" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +type SolvingData struct { + assignment WireAssignment + circuit gkrtypes.Circuit + workers *utils.WorkerPool +} + +func (d *SolvingData) init(info gkrtypes.SolvingInfo) { + d.workers = utils.NewWorkerPool() + d.circuit = info.Circuit + d.circuit.SetNbUniqueOutputs() + + d.assignment = make(WireAssignment, len(d.circuit)) + for i := range d.assignment { + d.assignment[i] = make([]fr.Element, info.NbInstances) + } +} + +// this module assumes that wire and instance indexes respect dependencies + +func setOuts(a WireAssignment, circuit gkrtypes.Circuit, outs []*big.Int) { + outsI := 0 + for i := range circuit { + if circuit[i].IsOutput() { + for j := range a[i] { + a[i][j].BigInt(outs[outsI]) + outsI++ + } + } + } + // Check if outsI == len(outs)? +} + +func SolveHint(info gkrtypes.SolvingInfo, data *SolvingData) hint.Hint { + return func(_ *big.Int, ins, outs []*big.Int) error { + // assumes assignmentVector is arranged wire first, instance second in order of solution + offsets := info.AssignmentOffsets() + data.init(info) + maxNIn := data.circuit.MaxGateNbIn() + + chunks := info.Chunks() + + solveTask := func(chunkOffset int) utils.Task { + return func(startInChunk, endInChunk int) { + start := startInChunk + chunkOffset + end := endInChunk + chunkOffset + inputs := make([]frontend.Variable, maxNIn) + dependencyHeads := make([]int, len(data.circuit)) // for each wire, which of its dependencies we would look at next + for wI := range data.circuit { // skip instances that are not relevant (related to instances before the current task) + deps := info.Dependencies[wI] + dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { + return deps[i].InputInstance + }, len(deps), start) + } + + for instanceI := start; instanceI < end; instanceI++ { + for wireI := range data.circuit { + wire := &data.circuit[wireI] + deps := info.Dependencies[wireI] + if wire.IsInput() { + if dependencyHeads[wireI] < len(deps) && instanceI == deps[dependencyHeads[wireI]].InputInstance { + dep := deps[dependencyHeads[wireI]] + data.assignment[wireI][instanceI].Set(&data.assignment[dep.OutputWire][dep.OutputInstance]) + dependencyHeads[wireI]++ + } else { + data.assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) + } + } else { + // assemble the inputs + inputIndexes := info.Circuit[wireI].Inputs + for i, inputI := range inputIndexes { + inputs[i] = &data.assignment[inputI][instanceI] + } + gate := data.circuit[wireI].Gate + data.assignment[wireI][instanceI].Set(gate.Evaluate(api, inputs[:len(inputIndexes)]...).(*fr.Element)) + } + } + } + } + } + + start := 0 + for _, end := range chunks { + data.workers.Submit(end-start, solveTask(start), 1024).Wait() + start = end + } + + for _, p := range info.Prints { + serializable := make([]any, len(p.Values)) + for i, v := range p.Values { + if p.IsGkrVar[i] { // serializer stores uint32 in slices as uint64 + serializable[i] = data.assignment[algo_utils.ForceUint32(v)][p.Instance].String() + } else { + serializable[i] = v + } + } + fmt.Println(serializable...) + } + + setOuts(data.assignment, info.Circuit, outs) + + return nil + } +} + +func ProveHint(hashName string, data *SolvingData) hint.Hint { + + return func(_ *big.Int, ins, outs []*big.Int) error { + insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called + b := make([]byte, fr.Bytes) + i.FillBytes(b) + return b[:] + }) + + hsh := hash.NewHash(hashName + "_BN254") + + proof, err := Prove(data.circuit, data.assignment, fiatshamir.WithHash(hsh, insBytes...), WithWorkers(data.workers)) + if err != nil { + return err + } + + return proof.SerializeToBigInts(outs) + + } +} diff --git a/internal/gkr/bn254/sumcheck.go b/internal/gkr/bn254/sumcheck.go new file mode 100644 index 00000000..d1efe090 --- /dev/null +++ b/internal/gkr/bn254/sumcheck.go @@ -0,0 +1,171 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "strconv" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +// This does not make use of parallelism and represents polynomials as lists of coefficients +// It is currently geared towards arithmetic hashes. Once we have a more unified hash function interface, this can be generified. + +// sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. +// Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) +type sumcheckClaims interface { + combine(a fr.Element) polynomial.Polynomial // combine into the 0ᵗʰ sumcheck subclaim. Create g := ∑_{1≤j≤m} aʲ⁻¹fⱼ for which now we seek to prove ∑_{0≤i<2ⁿ} g(i) = c := ∑_{1≤j≤m} aʲ⁻¹cⱼ. Return g₁. + next(fr.Element) polynomial.Polynomial // Return the evaluations gⱼ(k) for 1 ≤ k < degⱼ(g). Update the claim to gⱼ₊₁ for the input value as rⱼ + varsNum() int // number of variables + claimsNum() int // number of claims + proveFinalEval(r []fr.Element) []fr.Element // in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +// sumcheckLazyClaims is the sumcheckClaims data structure on the verifier side. It is "lazy" in that it has to compute fewer things. +type sumcheckLazyClaims interface { + claimsNum() int // claimsNum = m + varsNum() int // varsNum = n + combinedSum(a fr.Element) fr.Element // combinedSum returns c = ∑_{1≤j≤m} aʲ⁻¹cⱼ + degree(i int) int // degree of the total claim in the i'th variable + verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error +} + +// sumcheckProof of a multi-statement. +type sumcheckProof struct { + partialSumPolys []polynomial.Polynomial + finalEvalProof []fr.Element //in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +func setupTranscript(claimsNum int, varsNum int, settings *fiatshamir.Settings) (challengeNames []string, err error) { + numChallenges := varsNum + if claimsNum >= 2 { + numChallenges++ + } + challengeNames = make([]string, numChallenges) + if claimsNum >= 2 { + challengeNames[0] = settings.Prefix + "comb" + } + prefix := settings.Prefix + "pSP." + for i := 0; i < varsNum; i++ { + challengeNames[i+numChallenges-varsNum] = prefix + strconv.Itoa(i) + } + if settings.Transcript == nil { + transcript := fiatshamir.NewTranscript(settings.Hash, challengeNames...) + settings.Transcript = transcript + } + + for i := range settings.BaseChallenges { + if err = settings.Transcript.Bind(challengeNames[0], settings.BaseChallenges[i]); err != nil { + return + } + } + return +} + +func next(transcript *fiatshamir.Transcript, bindings []fr.Element, remainingChallengeNames *[]string) (fr.Element, error) { + challengeName := (*remainingChallengeNames)[0] + for i := range bindings { + bytes := bindings[i].Bytes() + if err := transcript.Bind(challengeName, bytes[:]); err != nil { + return fr.Element{}, err + } + } + var res fr.Element + bytes, err := transcript.ComputeChallenge(challengeName) + res.SetBytes(bytes) + + *remainingChallengeNames = (*remainingChallengeNames)[1:] + + return res, err +} + +// sumcheckProve create a non-interactive proof +func sumcheckProve(claims sumcheckClaims, transcriptSettings fiatshamir.Settings) (sumcheckProof, error) { + + var proof sumcheckProof + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return proof, err + } + + var combinationCoeff fr.Element + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return proof, err + } + } + + varsNum := claims.varsNum() + proof.partialSumPolys = make([]polynomial.Polynomial, varsNum) + proof.partialSumPolys[0] = claims.combine(combinationCoeff) + challenges := make([]fr.Element, varsNum) + + for j := 0; j+1 < varsNum; j++ { + if challenges[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return proof, err + } + proof.partialSumPolys[j+1] = claims.next(challenges[j]) + } + + if challenges[varsNum-1], err = next(transcript, proof.partialSumPolys[varsNum-1], &remainingChallengeNames); err != nil { + return proof, err + } + + proof.finalEvalProof = claims.proveFinalEval(challenges) + + return proof, nil +} + +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, transcriptSettings fiatshamir.Settings) error { + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return err + } + + var combinationCoeff fr.Element + + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return err + } + } + + r := make([]fr.Element, claims.varsNum()) + + // Just so that there is enough room for gJ to be reused + maxDegree := claims.degree(0) + for j := 1; j < claims.varsNum(); j++ { + if d := claims.degree(j); d > maxDegree { + maxDegree = d + } + } + gJ := make(polynomial.Polynomial, maxDegree+1) //At the end of iteration j, gJ = ∑_{i < 2ⁿ⁻ʲ⁻¹} g(X₁, ..., Xⱼ₊₁, i...) NOTE: n is shorthand for claims.varsNum() + gJR := claims.combinedSum(combinationCoeff) // At the beginning of iteration j, gJR = ∑_{i < 2ⁿ⁻ʲ} g(r₁, ..., rⱼ, i...) + + for j := range claims.varsNum() { + if len(proof.partialSumPolys[j]) != claims.degree(j) { + return errors.New("malformed proof") + } + copy(gJ[1:], proof.partialSumPolys[j]) + gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) // Requirement that gⱼ(0) + gⱼ(1) = gⱼ₋₁(r) + // gJ is ready + + //Prepare for the next iteration + if r[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return err + } + // This is an extremely inefficient way of interpolating. TODO: Interpolate without symbolically computing a polynomial + gJCoeffs := polynomial.InterpolateOnRange(gJ[:(claims.degree(j) + 1)]) + gJR = gJCoeffs.Eval(&r[j]) + } + + return claims.verifyFinalEval(r, combinationCoeff, gJR, proof.finalEvalProof) +} diff --git a/internal/gkr/bn254/sumcheck_test.go b/internal/gkr/bn254/sumcheck_test.go new file mode 100644 index 00000000..32eece33 --- /dev/null +++ b/internal/gkr/bn254/sumcheck_test.go @@ -0,0 +1,152 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/stretchr/testify/assert" + + "math/bits" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + + "strings" + "testing" +) + +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { + poly := make(polynomial.MultiLin, len(polyInt)) + for i, n := range polyInt { + poly[i].SetUint64(n) + } + + claim := singleMultilinClaim{g: poly.Clone()} + + proof, err := sumcheckProve(&claim, fiatshamir.WithHash(hashGenerator())) + if err != nil { + return err + } + + var sb strings.Builder + for _, p := range proof.partialSumPolys { + + sb.WriteString("\t{") + for i := 0; i < len(p); i++ { + sb.WriteString(p[i].String()) + if i+1 < len(p) { + sb.WriteString(", ") + } + } + sb.WriteString("}\n") + } + + lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if err = sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())); err != nil { + return err + } + + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())) == nil { + return fmt.Errorf("bad proof accepted") + } + return nil +} + +func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { + + polys := [][]uint64{ + {1, 2, 3, 4}, // 1 + 2X₁ + X₂ + {1, 2, 3, 4, 5, 6, 7, 8}, // 1 + 4X₁ + 2X₂ + X₃ + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, // 1 + 8X₁ + 4X₂ + 2X₃ + X₄ + } + + const MaxStep = 4 + const MaxStart = 4 + hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + + for step := 0; step < MaxStep; step++ { + for startState := 0; startState < MaxStart; startState++ { + if step == 0 && startState == 1 { // unlucky case where a bad proof would be accepted + continue + } + hashGens = append(hashGens, newMessageCounterGenerator(startState, step)) + } + } + + for _, poly := range polys { + for _, hashGen := range hashGens { + assert.NoError(t, testSumcheckSingleClaimMultilin(poly, hashGen), + "failed with poly %v and hashGen %v", poly, hashGen()) + } + } +} + +type singleMultilinClaim struct { + g polynomial.MultiLin +} + +func (c singleMultilinClaim) proveFinalEval(r []fr.Element) []fr.Element { + return nil // verifier can compute the final eval itself +} + +func (c singleMultilinClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} + +func (c singleMultilinClaim) claimsNum() int { + return 1 +} + +func sumForX1One(g polynomial.MultiLin) polynomial.Polynomial { + sum := g[len(g)/2] + for i := len(g)/2 + 1; i < len(g); i++ { + sum.Add(&sum, &g[i]) + } + return []fr.Element{sum} +} + +func (c singleMultilinClaim) combine(fr.Element) polynomial.Polynomial { + return sumForX1One(c.g) +} + +func (c *singleMultilinClaim) next(r fr.Element) polynomial.Polynomial { + c.g.Fold(r) + return sumForX1One(c.g) +} + +type singleMultilinLazyClaim struct { + g polynomial.MultiLin + claimedSum fr.Element +} + +func (c singleMultilinLazyClaim) verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error { + val := c.g.Evaluate(r, nil) + if val.Equal(&purportedValue) { + return nil + } + return fmt.Errorf("mismatch") +} + +func (c singleMultilinLazyClaim) combinedSum(combinationCoeffs fr.Element) fr.Element { + return c.claimedSum +} + +func (c singleMultilinLazyClaim) degree(i int) int { + return 1 +} + +func (c singleMultilinLazyClaim) claimsNum() int { + return 1 +} + +func (c singleMultilinLazyClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} diff --git a/internal/gkr/bn254/test_vector_utils_test.go b/internal/gkr/bn254/test_vector_utils_test.go new file mode 100644 index 00000000..82358426 --- /dev/null +++ b/internal/gkr/bn254/test_vector_utils_test.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "strings" + + "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark-crypto/ecc/bn254/fr/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrtesting" +) + +func toElement(i int64) *fr.Element { + var res fr.Element + res.SetInt64(i) + return &res +} + +func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { + if _type, ok := d["type"]; ok { + switch _type { + case "const": + startState := int64(d["val"].(float64)) + return &messageCounter{startState: startState, step: 0, state: startState}, nil + default: + return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) + } + } + return nil, fmt.Errorf("hash description missing type") +} + +type messageCounter struct { + startState int64 + state int64 + step int64 +} + +func (m *messageCounter) Write(p []byte) (n int, err error) { + inputBlockSize := (len(p)-1)/fr.Bytes + 1 + m.state += int64(inputBlockSize) * m.step + return len(p), nil +} + +func (m *messageCounter) Sum(b []byte) []byte { + inputBlockSize := (len(b)-1)/fr.Bytes + 1 + resI := m.state + int64(inputBlockSize)*m.step + var res fr.Element + res.SetInt64(int64(resI)) + resBytes := res.Bytes() + return resBytes[:] +} + +func (m *messageCounter) Reset() { + m.state = m.startState +} + +func (m *messageCounter) Size() int { + return fr.Bytes +} + +func (m *messageCounter) BlockSize() int { + return fr.Bytes +} + +func newMessageCounter(startState, step int) hash.Hash { + transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} + return transcript +} + +func newMessageCounterGenerator(startState, step int) func() hash.Hash { + return func() hash.Hash { + return newMessageCounter(startState, step) + } +} + +func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { + + // TODO: Put this in element.SetString? + switch v := value.(type) { + case string: + + if sep := strings.Split(v, "/"); len(sep) == 2 { + var denom fr.Element + if _, err := z.SetString(sep[0]); err != nil { + return nil, err + } + if _, err := denom.SetString(sep[1]); err != nil { + return nil, err + } + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil + } + + case float64: + asInt := int64(v) + if float64(asInt) != v { + return nil, fmt.Errorf("cannot currently parse float") + } + z.SetInt64(asInt) + return z, nil + } + + return z.SetInterface(value) +} + +func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { + elementSlice := make([]fr.Element, len(slice)) + for i, v := range slice { + if _, err := setElement(&elementSlice[i], v); err != nil { + return nil, err + } + } + return elementSlice, nil +} + +func sliceEquals(a []fr.Element, b []fr.Element) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if !a[i].Equal(&b[i]) { + return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) + } + } + return nil +} + +func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if err := sliceEquals(a[i], b[i]); err != nil { + return fmt.Errorf("at index %d: %w", i, err) + } + } + return nil +} diff --git a/internal/gkr/bw6-633/gate_testing.go b/internal/gkr/bw6-633/gate_testing.go new file mode 100644 index 00000000..0fafa45a --- /dev/null +++ b/internal/gkr/bw6-633/gate_testing.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + + "slices" + + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/polynomial" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// IsGateFunctionAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func IsGateFunctionAdditive(f gkr.GateFunction, i, nbIn int) bool { + fWrapped := api.convertFunc(f) + + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 := fWrapped(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := fWrapped(in...) + + y2.Sub(y2, y1) + y1.Sub(y1, y0) + + if !y2.Equal(y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 = fWrapped(in...) + + y1.Sub(y1, y0) + + return y1.Equal(y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f gateFunctionFr) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i].Set(f(fIn...)) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +// FindGateFunctionDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func FindGateFunctionDegree(f gkr.GateFunction, max, nbIn int) (int, error) { + fFr := api.convertFunc(f) + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := fFr.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, gkrtypes.ErrZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func VerifyGateFunctionDegree(f gkr.GateFunction, claimedDegree, nbIn int) error { + fFr := api.convertFunc(f) + if p := fFr.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return gkrtypes.ErrZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} diff --git a/internal/gkr/bw6-633/gkr.go b/internal/gkr/bw6-633/gkr.go new file mode 100644 index 00000000..cc1245e7 --- /dev/null +++ b/internal/gkr/bw6-633/gkr.go @@ -0,0 +1,816 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "sync" + + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// The goal is to prove/verify evaluations of many instances of the same circuit + +// WireAssignment is assignment of values to the same wire across many instances of the circuit +type WireAssignment []polynomial.MultiLin + +type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) + +// eqTimesGateEvalSumcheckLazyClaims is a lazy claim for sumcheck (verifier side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the checking of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckLazyClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ), allegedly + manager *claimsManager // WARNING: Circular references +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) getWire() *gkrtypes.Wire { + return e.manager.wires[e.wireI] +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) claimsNum() int { + return len(e.evaluationPoints) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) varsNum() int { + return len(e.evaluationPoints[0]) +} + +// combinedSum returns ∑ᵢ aⁱ yᵢ +func (e *eqTimesGateEvalSumcheckLazyClaims) combinedSum(a fr.Element) fr.Element { + evalsAsPoly := polynomial.Polynomial(e.claimedEvaluations) + return evalsAsPoly.Eval(&a) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) degree(int) int { + return 1 + e.manager.wires[e.wireI].Gate.Degree() +} + +// verifyFinalEval finalizes the verification of w. +// The prover's claims w(xᵢ) = yᵢ have already been reduced to verifying +// ∑ cⁱ eq(xᵢ, r) w(r) = purportedValue. ( c is combinationCoeff ) +// Both purportedValue and the vector r have been randomized during the sumcheck protocol. +// By taking the w term out of the sum we get the equivalent claim that +// for E := ∑ eq(xᵢ, r), it must be that E w(r) = purportedValue. +// If w is an input wire, the verifier can directly check its evaluation at r. +// Otherwise, the prover makes claims about the evaluation of w's input wires, +// wᵢ, at r, to be verified later. +// The claims are communicated through the proof parameter. +// The verifier checks here if the claimed evaluations of wᵢ(r) are consistent with +// the main claim, by checking E w(wᵢ(r)...) = purportedValue. +func (e *eqTimesGateEvalSumcheckLazyClaims) verifyFinalEval(r []fr.Element, combinationCoeff, purportedValue fr.Element, uniqueInputEvaluations []fr.Element) error { + // the eq terms ( E ) + numClaims := len(e.evaluationPoints) + evaluation := polynomial.EvalEq(e.evaluationPoints[numClaims-1], r) + for i := numClaims - 2; i >= 0; i-- { + evaluation.Mul(&evaluation, &combinationCoeff) + eq := polynomial.EvalEq(e.evaluationPoints[i], r) + evaluation.Add(&evaluation, &eq) + } + + wire := e.manager.wires[e.wireI] + + // the w(...) term + var gateEvaluation fr.Element + if wire.IsInput() { // just compute w(r) + gateEvaluation = e.manager.assignment[e.wireI].Evaluate(r, e.manager.memPool) + } else { // proof contains the evaluations of the inputs, but avoids repetition in case multiple inputs come from the same wire + injection, injectionLeftInv := + e.manager.wires.ClaimPropagationInfo(e.wireI) + + if len(injection) != len(uniqueInputEvaluations) { + return fmt.Errorf("%d input wire evaluations given, %d expected", len(uniqueInputEvaluations), len(injection)) + } + + for uniqueI, i := range injection { // map from unique to all + e.manager.add(wire.Inputs[i], r, uniqueInputEvaluations[uniqueI]) + } + + inputEvaluations := make([]frontend.Variable, len(wire.Inputs)) + for i, uniqueI := range injectionLeftInv { // map from all to unique + inputEvaluations[i] = &uniqueInputEvaluations[uniqueI] + } + + gateEvaluation.Set(wire.Gate.Evaluate(api, inputEvaluations...).(*fr.Element)) + } + + evaluation.Mul(&evaluation, &gateEvaluation) + + if evaluation.Equal(&purportedValue) { + return nil + } + return errors.New("incompatible evaluations") +} + +// eqTimesGateEvalSumcheckClaims is a claim for sumcheck (prover side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the proving of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ) + manager *claimsManager + + input []polynomial.MultiLin // input[i](h₁, ..., hₘ₋ⱼ) = wᵢ(r₁, r₂, ..., rⱼ₋₁, h₁, ..., hₘ₋ⱼ) + + eq polynomial.MultiLin // E := ∑ᵢ cⁱ eq(xᵢ, -) +} + +func (c *eqTimesGateEvalSumcheckClaims) getWire() *gkrtypes.Wire { + return c.manager.wires[c.wireI] +} + +// combine the multiple claims into one claim using a random combination (combinationCoeff or c). +// From the original multiple claims of w(xᵢ) = yᵢ, we get a single claim +// ∑ᵢ,ₕ cⁱ eq(xᵢ, h) w(h) = ∑ᵢ cⁱ yᵢ, where h iterates over the hypercube (circuit instances) and +// i iterates over the claims. +// Equivalently, we could say ∑ᵢ cⁱ yᵢ = ∑ₕ,ᵢ cⁱ eq(xᵢ, h) w(h) = ∑ₕ w(h) ∑ᵢ cⁱ eq(xᵢ, h). +// Thus if we initially compute E := ∑ᵢ cⁱ eq(xᵢ, -), our claim will find the simpler form +// ∑ᵢ cⁱ yᵢ = ∑ₕ w(h) E(h), where the sum-checked polynomial is of degree deg(g) + 1, +// and deg(g) is the total degree of the polynomial defining the gate g of which w is the output. +// The output of combine is the first sumcheck claim, i.e. ∑₍ₕ₁,ₕ₂,...₎ w(X, h₁, h₂, ...) E(X, h₁, h₂, ...).. +func (c *eqTimesGateEvalSumcheckClaims) combine(combinationCoeff fr.Element) polynomial.Polynomial { + varsNum := c.varsNum() + eqLength := 1 << varsNum + claimsNum := c.claimsNum() + // initialize the eq tables ( E ) + c.eq = c.manager.memPool.Make(eqLength) + + c.eq[0].SetOne() + c.eq.Eq(c.evaluationPoints[0]) + + // E := eq(x₀, -) + newEq := polynomial.MultiLin(c.manager.memPool.Make(eqLength)) + aI := combinationCoeff + + // E += cⁱ eq(xᵢ, -) + for k := 1; k < claimsNum; k++ { + newEq[0].Set(&aI) + + c.eqAcc(c.eq, newEq, c.evaluationPoints[k]) + + if k+1 < claimsNum { + aI.Mul(&aI, &combinationCoeff) + } + } + + c.manager.memPool.Dump(newEq) + + return c.computeGJ() +} + +// eqAcc sets m to an eq table at q and then adds it to e. +// m <- eq(q, -). +// e <- e + m +func (c *eqTimesGateEvalSumcheckClaims) eqAcc(e, m polynomial.MultiLin, q []fr.Element) { + n := len(q) + + //At the end of each iteration, m(h₁, ..., hₙ) = eq(q₁, ..., qᵢ₊₁, h₁, ..., hᵢ₊₁) + for i := range q { // In the comments we use a 1-based index so q[i] = qᵢ₊₁ + // go through all assignments of (b₁, ..., bᵢ) ∈ {0,1}ⁱ + const threshold = 1 << 6 + k := 1 << i + if k < threshold { + for j := 0; j < k; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + } else { + c.manager.workers.Submit(k, func(start, end int) { + for j := start; j < end; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + }, 1024).Wait() + } + + } + c.manager.workers.Submit(len(e), func(start, end int) { + for i := start; i < end; i++ { + e[i].Add(&e[i], &m[i]) + } + }, 512).Wait() +} + +// computeGJ: gⱼ = ∑_{0≤h<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, h...) = ∑_{0≤i<2ⁿ⁻ʲ} E(r₁, ..., Xⱼ, h...) g( w₀(r₁, ..., Xⱼ, h...), ... ). +// the polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). +// The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). By convention, g₀ is a constant polynomial equal to the claimed sum. +func (c *eqTimesGateEvalSumcheckClaims) computeGJ() polynomial.Polynomial { + + wire := c.getWire() + degGJ := 1 + wire.Gate.Degree() // guaranteed to be no smaller than the actual deg(gⱼ) + nbGateIn := len(c.input) + + // Both E and wᵢ (the input wires and the eq table) are multilinear, thus + // they are linear in Xⱼ. + // So for f ∈ { E(r₁, ..., Xⱼ, h...) } ∪ {wᵢ(r₁, ..., Xⱼ, h...) }, so f(m) = m×(f(1) - f(0)) + f(0), and f(0), f(1) are easily computed from the bookkeeping tables. + // ml are such multilinear polynomials the evaluations of which over different values of Xⱼ are computed in this stepwise manner. + ml := make([]polynomial.MultiLin, nbGateIn+1) // shortcut to the evaluations of the multilinear polynomials over the hypercube + ml[0] = c.eq + copy(ml[1:], c.input) + + sumSize := len(c.eq) / 2 // the range of h, over which we sum + + // Perf-TODO: Collate once at claim "combination" time and not again. then, even folding can be done in one operation every time "next" is called + + gJ := make([]fr.Element, degGJ) + var mu sync.Mutex + computeAll := func(start, end int) { // compute method to allow parallelization across instances + var step fr.Element + + res := make([]fr.Element, degGJ) + + // evaluations of ml, laid out as: + // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), + // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), + // ... + // ml[0](degGJ, h...), ml[2](degGJ, h...), ..., ml[len(ml)-1](degGJ, h...) + mlEvals := make([]fr.Element, degGJ*len(ml)) + gateInput := make([]frontend.Variable, nbGateIn) + + for h := start; h < end; h++ { // h counts across instances + + evalAt1Index := sumSize + h + for k := range ml { + // d = 0 + mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1. Can be taken directly from the table. + step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for d := 1; d < degGJ; d++ { + mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + } + } + + eIndex := 0 // index for where the current eq term is + nextEIndex := len(ml) + for d := range degGJ { + for i := range gateInput { + gateInput[i] = &mlEvals[eIndex+1+i] + } + summand := wire.Gate.Evaluate(api, gateInput...).(*fr.Element) + summand.Mul(summand, &mlEvals[eIndex]) + res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + } + } + mu.Lock() + for i := range gJ { + gJ[i].Add(&gJ[i], &res[i]) // collect into the complete sum + } + mu.Unlock() + } + + const minBlockSize = 64 + + if sumSize < minBlockSize { + // no parallelization + computeAll(0, sumSize) + } else { + c.manager.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } + + return gJ +} + +// next first folds the input and E polynomials at the given verifier challenge then computes the new gⱼ. +// Thus, j <- j+1 and rⱼ = challenge. +func (c *eqTimesGateEvalSumcheckClaims) next(challenge fr.Element) polynomial.Polynomial { + const minBlockSize = 512 + n := len(c.eq) / 2 + if n < minBlockSize { + // no parallelization + for i := 0; i < len(c.input); i++ { + c.input[i].Fold(challenge) + } + c.eq.Fold(challenge) + } else { + wgs := make([]*sync.WaitGroup, len(c.input)) + for i := 0; i < len(c.input); i++ { + wgs[i] = c.manager.workers.Submit(n, c.input[i].FoldParallel(challenge), minBlockSize) + } + c.manager.workers.Submit(n, c.eq.FoldParallel(challenge), minBlockSize).Wait() + for _, wg := range wgs { + wg.Wait() + } + } + + return c.computeGJ() +} + +func (c *eqTimesGateEvalSumcheckClaims) varsNum() int { + return len(c.evaluationPoints[0]) +} + +func (c *eqTimesGateEvalSumcheckClaims) claimsNum() int { + return len(c.claimedEvaluations) +} + +// proveFinalEval provides the values wᵢ(r₁, ..., rₙ) +func (c *eqTimesGateEvalSumcheckClaims) proveFinalEval(r []fr.Element) []fr.Element { + //defer the proof, return list of claims + + injection, _ := c.manager.wires.ClaimPropagationInfo(c.wireI) // TODO @Tabaie: Instead of doing this last, we could just have fewer input in the first place; not that likely to happen with single gates, but more so with layers. + evaluations := make([]fr.Element, len(injection)) + for i, gateInputI := range injection { + wI := c.input[gateInputI] + wI.Fold(r[len(r)-1]) // We already have wᵢ(r₁, ..., rₙ₋₁, hₙ) in a table. Only one more fold required. + c.manager.add(c.getWire().Inputs[gateInputI], r, wI[0]) + evaluations[i] = wI[0] + } + + c.manager.memPool.Dump(c.claimedEvaluations, c.eq) + + return evaluations +} + +type claimsManager struct { + claims []*eqTimesGateEvalSumcheckLazyClaims + assignment WireAssignment + memPool *polynomial.Pool + workers *utils.WorkerPool + wires gkrtypes.Wires +} + +func newClaimsManager(wires []*gkrtypes.Wire, assignment WireAssignment, o settings) (manager claimsManager) { + manager.assignment = assignment + manager.claims = make([]*eqTimesGateEvalSumcheckLazyClaims, len(wires)) + manager.memPool = o.pool + manager.workers = o.workers + manager.wires = wires + + for i, wire := range wires { + + manager.claims[i] = &eqTimesGateEvalSumcheckLazyClaims{ + wireI: i, + evaluationPoints: make([][]fr.Element, 0, wire.NbClaims()), + claimedEvaluations: manager.memPool.Make(wire.NbClaims()), + manager: &manager, + } + } + return +} + +func (m *claimsManager) add(wire int, evaluationPoint []fr.Element, evaluation fr.Element) { + claim := m.claims[wire] + i := len(claim.evaluationPoints) + claim.claimedEvaluations[i] = evaluation + claim.evaluationPoints = append(claim.evaluationPoints, evaluationPoint) +} + +func (m *claimsManager) getLazyClaim(wire int) *eqTimesGateEvalSumcheckLazyClaims { + return m.claims[wire] +} + +func (m *claimsManager) getClaim(wireI int) *eqTimesGateEvalSumcheckClaims { + lazy := m.claims[wireI] + wire := m.wires[wireI] + res := &eqTimesGateEvalSumcheckClaims{ + wireI: wireI, + evaluationPoints: lazy.evaluationPoints, + claimedEvaluations: lazy.claimedEvaluations, + manager: m, + } + + if wire.IsInput() { + res.input = []polynomial.MultiLin{m.memPool.Clone(m.assignment[wireI])} + } else { + res.input = make([]polynomial.MultiLin, len(wire.Inputs)) + + for inputI, inputW := range wire.Inputs { + res.input[inputI] = m.memPool.Clone(m.assignment[inputW]) //will be edited later, so must be deep copied + } + } + return res +} + +func (m *claimsManager) deleteClaim(wire int) { + m.claims[wire].manager = nil + m.claims[wire] = nil +} + +type settings struct { + pool *polynomial.Pool + sorted []*gkrtypes.Wire + transcript *fiatshamir.Transcript + transcriptPrefix string + nbVars int + workers *utils.WorkerPool +} + +type Option func(*settings) + +func WithPool(pool *polynomial.Pool) Option { + return func(options *settings) { + options.pool = pool + } +} + +func WithSortedCircuit(sorted []*gkrtypes.Wire) Option { + return func(options *settings) { + options.sorted = sorted + } +} + +func WithWorkers(workers *utils.WorkerPool) Option { + return func(options *settings) { + options.workers = workers + } +} + +func setup(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (settings, error) { + var o settings + var err error + for _, option := range options { + option(&o) + } + + o.nbVars = assignment.NumVars() + nbInstances := assignment.NumInstances() + if 1< 1 { //combine the claims + size++ + } + size += logNbInstances // full run of sumcheck on logNbInstances variables + } + + nums := make([]string, max(len(sorted), logNbInstances)) + for i := range nums { + nums[i] = strconv.Itoa(i) + } + + challenges := make([]string, size) + + // output wire claims + firstChallengePrefix := prefix + "fC." + for j := 0; j < logNbInstances; j++ { + challenges[j] = firstChallengePrefix + nums[j] + } + j := logNbInstances + for i := len(sorted) - 1; i >= 0; i-- { + if sorted[i].NoProof() { + continue + } + wirePrefix := prefix + "w" + nums[i] + "." + + if sorted[i].NbClaims() > 1 { + challenges[j] = wirePrefix + "comb" + j++ + } + + partialSumPrefix := wirePrefix + "pSP." + for k := 0; k < logNbInstances; k++ { + challenges[j] = partialSumPrefix + nums[k] + j++ + } + } + return challenges +} + +func getFirstChallengeNames(logNbInstances int, prefix string) []string { + res := make([]string, logNbInstances) + firstChallengePrefix := prefix + "fC." + for i := 0; i < logNbInstances; i++ { + res[i] = firstChallengePrefix + strconv.Itoa(i) + } + return res +} + +func getChallenges(transcript *fiatshamir.Transcript, names []string) ([]fr.Element, error) { + res := make([]fr.Element, len(names)) + for i, name := range names { + if bytes, err := transcript.ComputeChallenge(name); err == nil { + res[i].SetBytes(bytes) + } else { + return nil, err + } + } + return res, nil +} + +// Prove consistency of the claimed assignment +func Prove(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (Proof, error) { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return nil, err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + proof := make(Proof, len(c)) + // firstChallenge called rho in the paper + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return nil, err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + claim := claims.getClaim(i) + if wire.NoProof() { // input wires with one claim only + proof[i] = sumcheckProof{ + partialSumPolys: []polynomial.Polynomial{}, + finalEvalProof: []fr.Element{}, + } + } else { + if proof[i], err = sumcheckProve( + claim, fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err != nil { + return proof, err + } + + baseChallenge = make([][]byte, len(proof[i].finalEvalProof)) + for j := range proof[i].finalEvalProof { + baseChallenge[j] = proof[i].finalEvalProof[j].Marshal() + } + } + // the verifier checks a single claim about input wires itself + claims.deleteClaim(i) + } + + return proof, nil +} + +// Verify the consistency of the claimed output with the claimed input +// Unlike in Prove, the assignment argument need not be complete +func Verify(c gkrtypes.Circuit, assignment WireAssignment, proof Proof, transcriptSettings fiatshamir.Settings, options ...Option) error { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + proofW := proof[i] + claim := claims.getLazyClaim(i) + if wire.NoProof() { // input wires with one claim only + // make sure the proof is empty + if len(proofW.finalEvalProof) != 0 || len(proofW.partialSumPolys) != 0 { + return errors.New("no proof allowed for input wire with a single claim") + } + + if wire.NbClaims() == 1 { // input wire + // simply evaluate and see if it matches + evaluation := assignment[i].Evaluate(claim.evaluationPoints[0], claims.memPool) + if !claim.claimedEvaluations[0].Equal(&evaluation) { + return errors.New("incorrect input wire claim") + } + } + } else if err = sumcheckVerify( + claim, proof[i], fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err == nil { // incorporate prover claims about w's input into the transcript + baseChallenge = make([][]byte, len(proofW.finalEvalProof)) + for j := range baseChallenge { + baseChallenge[j] = proofW.finalEvalProof[j].Marshal() + } + } else { + return fmt.Errorf("sumcheck proof rejected: %v", err) //TODO: Any polynomials to dump? + } + claims.deleteClaim(i) + } + return nil +} + +// Complete the circuit evaluation from input values +func (a WireAssignment) Complete(wires gkrtypes.Wires) WireAssignment { + + nbInstances := a.NumInstances() + maxNbIns := 0 + + for i, w := range wires { + maxNbIns = max(maxNbIns, len(w.Inputs)) + if len(a[i]) != nbInstances { + a[i] = make([]fr.Element, nbInstances) + } + } + + ins := make([]fr.Element, maxNbIns) + for i := range nbInstances { + for wI, w := range wires { + if !w.IsInput() { + for inI, in := range w.Inputs { + ins[inI] = a[in][i] + } + a[wI][i].Set(api.evaluate(w.Gate.Evaluate, ins[:len(w.Inputs)]...)) + } + } + } + + return a +} + +func (a WireAssignment) NumInstances() int { + for _, aW := range a { + return len(aW) + } + panic("empty assignment") +} + +func (a WireAssignment) NumVars() int { + for _, aW := range a { + return aW.NumVars() + } + panic("empty assignment") +} + +// SerializeToBigInts flattens a proof object into the given slice of big.Ints +// useful in gnark hints. +func (p Proof) SerializeToBigInts(outs []*big.Int) error { + offset := 0 + for i := range p { + for _, poly := range p[i].partialSumPolys { + frToBigInts(outs[offset:], poly) + offset += len(poly) + } + if p[i].finalEvalProof != nil { + frToBigInts(outs[offset:], p[i].finalEvalProof) + offset += len(p[i].finalEvalProof) + } + } + if offset != len(outs) { + return fmt.Errorf("expected %d elements, got %d", offset, len(outs)) + } + return nil +} + +func frToBigInts(dst []*big.Int, src []fr.Element) { + for i := range src { + src[i].BigInt(dst[i]) + } +} + +// gateAPI implements gkr.GateAPI. +type gateAPI struct{} + +var api gateAPI + +func (gateAPI) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element // TODO Heap allocated. Keep an eye on perf + res.Add(cast(i1), cast(i2)) + for _, v := range in { + res.Add(&res, cast(v)) + } + return &res +} + +func (gateAPI) MulAcc(a, b, c frontend.Variable) frontend.Variable { + var prod fr.Element + prod.Add(cast(b), cast(c)) + res := cast(a) + res.Add(res, &prod) + return &res +} + +func (gateAPI) Neg(i1 frontend.Variable) frontend.Variable { + var res fr.Element + res.Neg(cast(i1)) + return &res +} + +func (gateAPI) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Sub(cast(i1), cast(i2)) + for _, v := range in { + res.Sub(&res, cast(v)) + } + return &res +} + +func (gateAPI) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Mul(cast(i1), cast(i2)) + for _, v := range in { + res.Mul(&res, cast(v)) + } + return &res +} + +func (gateAPI) Println(a ...frontend.Variable) { + toPrint := make([]any, len(a)) + var x fr.Element + + for i, v := range a { + if _, err := x.SetInterface(v); err != nil { + toPrint[i] = x.String() + } else { + if s, ok := v.(string); ok { + toPrint[i] = s + continue + } + panic(fmt.Errorf("not numeric or string: %w", err)) + } + } + fmt.Println(toPrint...) +} + +func (api gateAPI) evaluate(f gkr.GateFunction, in ...fr.Element) *fr.Element { + inVar := make([]frontend.Variable, len(in)) + for i := range in { + inVar[i] = &in[i] + } + return f(api, inVar...).(*fr.Element) +} + +type gateFunctionFr func(...fr.Element) *fr.Element + +// convertFunc turns f into a function that accepts and returns fr.Element. +func (api gateAPI) convertFunc(f gkr.GateFunction) gateFunctionFr { + return func(in ...fr.Element) *fr.Element { + return api.evaluate(f, in...) + } +} + +func cast(v frontend.Variable) *fr.Element { + if x, ok := v.(*fr.Element); ok { // fast path, no extra heap allocation + return x + } + var x fr.Element + if _, err := x.SetInterface(v); err != nil { + panic(err) + } + return &x +} diff --git a/internal/gkr/bw6-633/gkr_test.go b/internal/gkr/bw6-633/gkr_test.go new file mode 100644 index 00000000..20ad407e --- /dev/null +++ b/internal/gkr/bw6-633/gkr_test.go @@ -0,0 +1,505 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "os" + "path/filepath" + "reflect" + "strconv" + "testing" + "time" + + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/mimc" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + gcUtils "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/internal/utils" + "github.com/consensys/gnark/std/gkrapi/gkr" + "github.com/stretchr/testify/assert" +) + +func TestNoGateTwoInstances(t *testing.T) { + // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case + testNoGate(t, []fr.Element{four, three}) +} + +func TestNoGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}}) +} + +func TestSingleAddGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Add2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleMulGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Mul2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleInputTwoIdentityGates(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + }) +} + +func TestSingleInputTwoIdentityGatesComposed(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{1}, + }}) +} + +func TestAPowNTimesBCircuit(t *testing.T) { + const N = 10 + + c := make(gkrtypes.Circuit, N+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: gkrtypes.Mul2(), + Inputs: []int{i - 1, 0}, + } + } + + test(t, c) +} + +func TestSingleMimcCipherGate(t *testing.T) { + test(t, gkrtypes.Circuit{ + {}, {}, + { + Inputs: []int{0, 1}, + Gate: cache.GetGate("mimc"), + }, + }) +} + +func TestShallowMimcTwoInstances(t *testing.T) { + test(t, mimcCircuit(2)) +} + +func TestMimc(t *testing.T) { + test(t, mimcCircuit(93)) +} + +func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { + circuit := gkrtypes.Circuit{gkrtypes.Wire{ + Gate: gkrtypes.Identity(), + NbUniqueOutputs: 2, + }} + + assignment := WireAssignment{[]fr.Element{two, three}} + var o settings + pool := polynomial.NewPool(256, 1<<11) + workers := gcUtils.NewWorkerPool() + o.pool = &pool + o.workers = workers + + claimsManagerGen := func() *claimsManager { + manager := newClaimsManager(utils.References(circuit), assignment, o) + manager.add(0, []fr.Element{three}, five) + manager.add(0, []fr.Element{four}, six) + return &manager + } + + transcriptGen := newMessageCounterGenerator(4, 1) + + proof, err := sumcheckProve(claimsManagerGen().getClaim(0), fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) + err = sumcheckVerify(claimsManagerGen().getLazyClaim(0), proof, fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) +} + +var one, two, three, four, five, six fr.Element + +func init() { + one.SetOne() + two.Double(&one) + three.Add(&two, &one) + four.Double(&two) + five.Add(&three, &two) + six.Double(&three) +} + +var testManyInstancesLogMaxInstances = -1 + +func getLogMaxInstances(t *testing.T) int { + if testManyInstancesLogMaxInstances == -1 { + + s := os.Getenv("GKR_LOG_INSTANCES") + if s == "" { + testManyInstancesLogMaxInstances = 5 + } else { + var err error + testManyInstancesLogMaxInstances, err = strconv.Atoi(s) + if err != nil { + t.Error(err) + } + } + + } + return testManyInstancesLogMaxInstances +} + +func test(t *testing.T, circuit gkrtypes.Circuit) { + wireRefs := utils.References(circuit) + ins := circuit.Inputs() + insAssignment := make(WireAssignment, len(ins)) + maxSize := 1 << getLogMaxInstances(t) + + for i := range ins { + insAssignment[i] = make([]fr.Element, maxSize) + fr.Vector(insAssignment[i]).MustSetRandom() + } + + fullAssignment := make(WireAssignment, len(circuit)) + for _, numEvals := range []int{2, maxSize} { + for i := range ins { + fullAssignment[ins[i]] = insAssignment[i][:numEvals] + } + + fullAssignment.Complete(wireRefs) + + t.Log("Selected inputs for test") + + proof, err := Prove(circuit, fullAssignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") + + if proof.isEmpty() { // special case for TestNoGate: + continue // there's no way to make a trivial proof fail + } + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(0, 1))) + assert.NotNil(t, err, "bad proof accepted") + } + +} + +func (p Proof) isEmpty() bool { + for i := range p { + if len(p[i].finalEvalProof) != 0 { + return false + } + for j := range p[i].partialSumPolys { + if len(p[i].partialSumPolys[j]) != 0 { + return false + } + } + } + return true +} + +func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { + c := gkrtypes.Circuit{ + {}, + } + + assignment := WireAssignment{0: inputAssignments[0]} + + proof, err := Prove(c, assignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(c, assignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") +} + +func mimcCircuit(numRounds int) gkrtypes.Circuit { + c := make(gkrtypes.Circuit, numRounds+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: cache.GetGate("mimc"), + Inputs: []int{i - 1, 0}, + } + } + return c +} + +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("bivariate input needed") + } + res := api.Add(x[0], x[1]) + return api.Mul(res, x[0]) + } + + // g: x,y -> x² + 3y + g := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + res := api.Mul(x[0], x[0]) + y3 := api.Mul(x[1], 3) + return api.Add(res, y3) + } + + // h: x -> 2x + // but it edits it input + h := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + return api.Add(x[0], x[0]) + } + + assert.False(t, IsGateFunctionAdditive(f, 1, 2)) + assert.False(t, IsGateFunctionAdditive(f, 0, 2)) + + assert.False(t, IsGateFunctionAdditive(g, 0, 2)) + assert.True(t, IsGateFunctionAdditive(g, 1, 2)) + + assert.True(t, IsGateFunctionAdditive(h, 0, 1)) +} + +func generateTestProver(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + proof, err := Prove(testCase.Circuit, testCase.FullAssignment, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err) + assert.NoError(t, proofEquals(testCase.Proof, proof)) + } +} + +func generateTestVerifier(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err, "proof rejected") + testCase, err = newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(newMessageCounter(2, 0))) + assert.NotNil(t, err, "bad proof accepted") + } +} + +func TestGkrVectors(t *testing.T) { + + const testDirPath = "../test_vectors/" + dirEntries, err := os.ReadDir(testDirPath) + assert.NoError(t, err) + for _, dirEntry := range dirEntries { + if !dirEntry.IsDir() { + + if filepath.Ext(dirEntry.Name()) == ".json" { + path := filepath.Join(testDirPath, dirEntry.Name()) + noExt := dirEntry.Name()[:len(dirEntry.Name())-len(".json")] + + t.Run(noExt+"_prover", generateTestProver(path)) + t.Run(noExt+"_verifier", generateTestVerifier(path)) + + } + } + } +} + +func proofEquals(expected Proof, seen Proof) error { + if len(expected) != len(seen) { + return fmt.Errorf("length mismatch %d ≠ %d", len(expected), len(seen)) + } + for i, x := range expected { + xSeen := seen[i] + + if xSeen.finalEvalProof == nil { + if seenFinalEval := x.finalEvalProof; len(seenFinalEval) != 0 { + return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) + } + } else { + if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + return fmt.Errorf("final evaluation proof mismatch") + } + } + if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + return err + } + } + return nil +} + +func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { + fmt.Println("creating circuit structure") + c := mimcCircuit(mimcDepth) + + in0 := make([]fr.Element, nbInstances) + in1 := make([]fr.Element, nbInstances) + fr.Vector(in0).MustSetRandom() + fr.Vector(in1).MustSetRandom() + + fmt.Println("evaluating circuit") + start := time.Now().UnixMicro() + assignment := WireAssignment{in0, in1}.Complete(utils.References(c)) + solved := time.Now().UnixMicro() - start + fmt.Println("solved in", solved, "μs") + + //b.ResetTimer() + fmt.Println("constructing proof") + start = time.Now().UnixMicro() + _, err := Prove(c, assignment, fiatshamir.WithHash(mimc.NewMiMC())) + proved := time.Now().UnixMicro() - start + fmt.Println("proved in", proved, "μs") + assert.NoError(b, err) +} + +func BenchmarkGkrMimc19(b *testing.B) { + benchmarkGkrMiMC(b, 1<<19, 91) +} + +func BenchmarkGkrMimc17(b *testing.B) { + benchmarkGkrMiMC(b, 1<<17, 91) +} + +func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { + proof := make(Proof, len(printable)) + for i := range printable { + finalEvalProof := []fr.Element(nil) + + if printable[i].FinalEvalProof != nil { + finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) + finalEvalProof = make([]fr.Element, finalEvalSlice.Len()) + for k := range finalEvalProof { + if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + return nil, err + } + } + } + + proof[i] = sumcheckProof{ + partialSumPolys: make([]polynomial.Polynomial, len(printable[i].PartialSumPolys)), + finalEvalProof: finalEvalProof, + } + for k := range printable[i].PartialSumPolys { + var err error + if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + return nil, err + } + } + } + return proof, nil +} + +type TestCase struct { + Circuit gkrtypes.Circuit + Hash hash.Hash + Proof Proof + FullAssignment WireAssignment + InOutAssignment WireAssignment +} + +var ( + testCases = make(map[string]*TestCase) + cache = gkrtesting.NewCache() +) + +func newTestCase(path string) (*TestCase, error) { + path, err := filepath.Abs(path) + if err != nil { + return nil, err + } + dir := filepath.Dir(path) + + tCase, ok := testCases[path] + if ok { + return tCase, nil + } + + info, err := cache.ReadTestCaseInfo(path) + if err != nil { + return nil, err + } + + circuit := cache.GetCircuit(filepath.Join(dir, info.Circuit)) + var _hash hash.Hash + if _hash, err = hashFromDescription(info.Hash); err != nil { + return nil, err + } + var proof Proof + if proof, err = unmarshalProof(info.Proof); err != nil { + return nil, err + } + + fullAssignment := make(WireAssignment, len(circuit)) + inOutAssignment := make(WireAssignment, len(circuit)) + + sorted := circuit.TopologicalSort() + + inI, outI := 0, 0 + for i, w := range sorted { + var assignmentRaw []interface{} + if w.IsInput() { + if inI == len(info.Input) { + return nil, fmt.Errorf("fewer input in vector than in circuit") + } + assignmentRaw = info.Input[inI] + inI++ + } else if w.IsOutput() { + if outI == len(info.Output) { + return nil, fmt.Errorf("fewer output in vector than in circuit") + } + assignmentRaw = info.Output[outI] + outI++ + } + if assignmentRaw != nil { + var wireAssignment []fr.Element + if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + return nil, err + } + + fullAssignment[i] = wireAssignment + inOutAssignment[i] = wireAssignment + } + } + + fullAssignment.Complete(utils.References(circuit)) + + for i, w := range sorted { + if w.IsOutput() { + if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + return nil, fmt.Errorf("assignment mismatch: %v", err) + } + } + } + + tCase = &TestCase{ + FullAssignment: fullAssignment, + InOutAssignment: inOutAssignment, + Proof: proof, + Hash: _hash, + Circuit: circuit, + } + + testCases[path] = tCase + + return tCase, nil +} diff --git a/internal/gkr/bw6-633/solver_hints.go b/internal/gkr/bw6-633/solver_hints.go new file mode 100644 index 00000000..57343d29 --- /dev/null +++ b/internal/gkr/bw6-633/solver_hints.go @@ -0,0 +1,146 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark-crypto/hash" + "github.com/consensys/gnark-crypto/utils" + hint "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + algo_utils "github.com/consensys/gnark/internal/utils" + + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +type SolvingData struct { + assignment WireAssignment + circuit gkrtypes.Circuit + workers *utils.WorkerPool +} + +func (d *SolvingData) init(info gkrtypes.SolvingInfo) { + d.workers = utils.NewWorkerPool() + d.circuit = info.Circuit + d.circuit.SetNbUniqueOutputs() + + d.assignment = make(WireAssignment, len(d.circuit)) + for i := range d.assignment { + d.assignment[i] = make([]fr.Element, info.NbInstances) + } +} + +// this module assumes that wire and instance indexes respect dependencies + +func setOuts(a WireAssignment, circuit gkrtypes.Circuit, outs []*big.Int) { + outsI := 0 + for i := range circuit { + if circuit[i].IsOutput() { + for j := range a[i] { + a[i][j].BigInt(outs[outsI]) + outsI++ + } + } + } + // Check if outsI == len(outs)? +} + +func SolveHint(info gkrtypes.SolvingInfo, data *SolvingData) hint.Hint { + return func(_ *big.Int, ins, outs []*big.Int) error { + // assumes assignmentVector is arranged wire first, instance second in order of solution + offsets := info.AssignmentOffsets() + data.init(info) + maxNIn := data.circuit.MaxGateNbIn() + + chunks := info.Chunks() + + solveTask := func(chunkOffset int) utils.Task { + return func(startInChunk, endInChunk int) { + start := startInChunk + chunkOffset + end := endInChunk + chunkOffset + inputs := make([]frontend.Variable, maxNIn) + dependencyHeads := make([]int, len(data.circuit)) // for each wire, which of its dependencies we would look at next + for wI := range data.circuit { // skip instances that are not relevant (related to instances before the current task) + deps := info.Dependencies[wI] + dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { + return deps[i].InputInstance + }, len(deps), start) + } + + for instanceI := start; instanceI < end; instanceI++ { + for wireI := range data.circuit { + wire := &data.circuit[wireI] + deps := info.Dependencies[wireI] + if wire.IsInput() { + if dependencyHeads[wireI] < len(deps) && instanceI == deps[dependencyHeads[wireI]].InputInstance { + dep := deps[dependencyHeads[wireI]] + data.assignment[wireI][instanceI].Set(&data.assignment[dep.OutputWire][dep.OutputInstance]) + dependencyHeads[wireI]++ + } else { + data.assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) + } + } else { + // assemble the inputs + inputIndexes := info.Circuit[wireI].Inputs + for i, inputI := range inputIndexes { + inputs[i] = &data.assignment[inputI][instanceI] + } + gate := data.circuit[wireI].Gate + data.assignment[wireI][instanceI].Set(gate.Evaluate(api, inputs[:len(inputIndexes)]...).(*fr.Element)) + } + } + } + } + } + + start := 0 + for _, end := range chunks { + data.workers.Submit(end-start, solveTask(start), 1024).Wait() + start = end + } + + for _, p := range info.Prints { + serializable := make([]any, len(p.Values)) + for i, v := range p.Values { + if p.IsGkrVar[i] { // serializer stores uint32 in slices as uint64 + serializable[i] = data.assignment[algo_utils.ForceUint32(v)][p.Instance].String() + } else { + serializable[i] = v + } + } + fmt.Println(serializable...) + } + + setOuts(data.assignment, info.Circuit, outs) + + return nil + } +} + +func ProveHint(hashName string, data *SolvingData) hint.Hint { + + return func(_ *big.Int, ins, outs []*big.Int) error { + insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called + b := make([]byte, fr.Bytes) + i.FillBytes(b) + return b[:] + }) + + hsh := hash.NewHash(hashName + "_BW6_633") + + proof, err := Prove(data.circuit, data.assignment, fiatshamir.WithHash(hsh, insBytes...), WithWorkers(data.workers)) + if err != nil { + return err + } + + return proof.SerializeToBigInts(outs) + + } +} diff --git a/internal/gkr/bw6-633/sumcheck.go b/internal/gkr/bw6-633/sumcheck.go new file mode 100644 index 00000000..8779588c --- /dev/null +++ b/internal/gkr/bw6-633/sumcheck.go @@ -0,0 +1,171 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "strconv" + + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +// This does not make use of parallelism and represents polynomials as lists of coefficients +// It is currently geared towards arithmetic hashes. Once we have a more unified hash function interface, this can be generified. + +// sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. +// Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) +type sumcheckClaims interface { + combine(a fr.Element) polynomial.Polynomial // combine into the 0ᵗʰ sumcheck subclaim. Create g := ∑_{1≤j≤m} aʲ⁻¹fⱼ for which now we seek to prove ∑_{0≤i<2ⁿ} g(i) = c := ∑_{1≤j≤m} aʲ⁻¹cⱼ. Return g₁. + next(fr.Element) polynomial.Polynomial // Return the evaluations gⱼ(k) for 1 ≤ k < degⱼ(g). Update the claim to gⱼ₊₁ for the input value as rⱼ + varsNum() int // number of variables + claimsNum() int // number of claims + proveFinalEval(r []fr.Element) []fr.Element // in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +// sumcheckLazyClaims is the sumcheckClaims data structure on the verifier side. It is "lazy" in that it has to compute fewer things. +type sumcheckLazyClaims interface { + claimsNum() int // claimsNum = m + varsNum() int // varsNum = n + combinedSum(a fr.Element) fr.Element // combinedSum returns c = ∑_{1≤j≤m} aʲ⁻¹cⱼ + degree(i int) int // degree of the total claim in the i'th variable + verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error +} + +// sumcheckProof of a multi-statement. +type sumcheckProof struct { + partialSumPolys []polynomial.Polynomial + finalEvalProof []fr.Element //in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +func setupTranscript(claimsNum int, varsNum int, settings *fiatshamir.Settings) (challengeNames []string, err error) { + numChallenges := varsNum + if claimsNum >= 2 { + numChallenges++ + } + challengeNames = make([]string, numChallenges) + if claimsNum >= 2 { + challengeNames[0] = settings.Prefix + "comb" + } + prefix := settings.Prefix + "pSP." + for i := 0; i < varsNum; i++ { + challengeNames[i+numChallenges-varsNum] = prefix + strconv.Itoa(i) + } + if settings.Transcript == nil { + transcript := fiatshamir.NewTranscript(settings.Hash, challengeNames...) + settings.Transcript = transcript + } + + for i := range settings.BaseChallenges { + if err = settings.Transcript.Bind(challengeNames[0], settings.BaseChallenges[i]); err != nil { + return + } + } + return +} + +func next(transcript *fiatshamir.Transcript, bindings []fr.Element, remainingChallengeNames *[]string) (fr.Element, error) { + challengeName := (*remainingChallengeNames)[0] + for i := range bindings { + bytes := bindings[i].Bytes() + if err := transcript.Bind(challengeName, bytes[:]); err != nil { + return fr.Element{}, err + } + } + var res fr.Element + bytes, err := transcript.ComputeChallenge(challengeName) + res.SetBytes(bytes) + + *remainingChallengeNames = (*remainingChallengeNames)[1:] + + return res, err +} + +// sumcheckProve create a non-interactive proof +func sumcheckProve(claims sumcheckClaims, transcriptSettings fiatshamir.Settings) (sumcheckProof, error) { + + var proof sumcheckProof + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return proof, err + } + + var combinationCoeff fr.Element + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return proof, err + } + } + + varsNum := claims.varsNum() + proof.partialSumPolys = make([]polynomial.Polynomial, varsNum) + proof.partialSumPolys[0] = claims.combine(combinationCoeff) + challenges := make([]fr.Element, varsNum) + + for j := 0; j+1 < varsNum; j++ { + if challenges[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return proof, err + } + proof.partialSumPolys[j+1] = claims.next(challenges[j]) + } + + if challenges[varsNum-1], err = next(transcript, proof.partialSumPolys[varsNum-1], &remainingChallengeNames); err != nil { + return proof, err + } + + proof.finalEvalProof = claims.proveFinalEval(challenges) + + return proof, nil +} + +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, transcriptSettings fiatshamir.Settings) error { + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return err + } + + var combinationCoeff fr.Element + + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return err + } + } + + r := make([]fr.Element, claims.varsNum()) + + // Just so that there is enough room for gJ to be reused + maxDegree := claims.degree(0) + for j := 1; j < claims.varsNum(); j++ { + if d := claims.degree(j); d > maxDegree { + maxDegree = d + } + } + gJ := make(polynomial.Polynomial, maxDegree+1) //At the end of iteration j, gJ = ∑_{i < 2ⁿ⁻ʲ⁻¹} g(X₁, ..., Xⱼ₊₁, i...) NOTE: n is shorthand for claims.varsNum() + gJR := claims.combinedSum(combinationCoeff) // At the beginning of iteration j, gJR = ∑_{i < 2ⁿ⁻ʲ} g(r₁, ..., rⱼ, i...) + + for j := range claims.varsNum() { + if len(proof.partialSumPolys[j]) != claims.degree(j) { + return errors.New("malformed proof") + } + copy(gJ[1:], proof.partialSumPolys[j]) + gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) // Requirement that gⱼ(0) + gⱼ(1) = gⱼ₋₁(r) + // gJ is ready + + //Prepare for the next iteration + if r[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return err + } + // This is an extremely inefficient way of interpolating. TODO: Interpolate without symbolically computing a polynomial + gJCoeffs := polynomial.InterpolateOnRange(gJ[:(claims.degree(j) + 1)]) + gJR = gJCoeffs.Eval(&r[j]) + } + + return claims.verifyFinalEval(r, combinationCoeff, gJR, proof.finalEvalProof) +} diff --git a/internal/gkr/bw6-633/sumcheck_test.go b/internal/gkr/bw6-633/sumcheck_test.go new file mode 100644 index 00000000..b3a35c9c --- /dev/null +++ b/internal/gkr/bw6-633/sumcheck_test.go @@ -0,0 +1,152 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/stretchr/testify/assert" + + "math/bits" + + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" + + "strings" + "testing" +) + +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { + poly := make(polynomial.MultiLin, len(polyInt)) + for i, n := range polyInt { + poly[i].SetUint64(n) + } + + claim := singleMultilinClaim{g: poly.Clone()} + + proof, err := sumcheckProve(&claim, fiatshamir.WithHash(hashGenerator())) + if err != nil { + return err + } + + var sb strings.Builder + for _, p := range proof.partialSumPolys { + + sb.WriteString("\t{") + for i := 0; i < len(p); i++ { + sb.WriteString(p[i].String()) + if i+1 < len(p) { + sb.WriteString(", ") + } + } + sb.WriteString("}\n") + } + + lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if err = sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())); err != nil { + return err + } + + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())) == nil { + return fmt.Errorf("bad proof accepted") + } + return nil +} + +func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { + + polys := [][]uint64{ + {1, 2, 3, 4}, // 1 + 2X₁ + X₂ + {1, 2, 3, 4, 5, 6, 7, 8}, // 1 + 4X₁ + 2X₂ + X₃ + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, // 1 + 8X₁ + 4X₂ + 2X₃ + X₄ + } + + const MaxStep = 4 + const MaxStart = 4 + hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + + for step := 0; step < MaxStep; step++ { + for startState := 0; startState < MaxStart; startState++ { + if step == 0 && startState == 1 { // unlucky case where a bad proof would be accepted + continue + } + hashGens = append(hashGens, newMessageCounterGenerator(startState, step)) + } + } + + for _, poly := range polys { + for _, hashGen := range hashGens { + assert.NoError(t, testSumcheckSingleClaimMultilin(poly, hashGen), + "failed with poly %v and hashGen %v", poly, hashGen()) + } + } +} + +type singleMultilinClaim struct { + g polynomial.MultiLin +} + +func (c singleMultilinClaim) proveFinalEval(r []fr.Element) []fr.Element { + return nil // verifier can compute the final eval itself +} + +func (c singleMultilinClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} + +func (c singleMultilinClaim) claimsNum() int { + return 1 +} + +func sumForX1One(g polynomial.MultiLin) polynomial.Polynomial { + sum := g[len(g)/2] + for i := len(g)/2 + 1; i < len(g); i++ { + sum.Add(&sum, &g[i]) + } + return []fr.Element{sum} +} + +func (c singleMultilinClaim) combine(fr.Element) polynomial.Polynomial { + return sumForX1One(c.g) +} + +func (c *singleMultilinClaim) next(r fr.Element) polynomial.Polynomial { + c.g.Fold(r) + return sumForX1One(c.g) +} + +type singleMultilinLazyClaim struct { + g polynomial.MultiLin + claimedSum fr.Element +} + +func (c singleMultilinLazyClaim) verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error { + val := c.g.Evaluate(r, nil) + if val.Equal(&purportedValue) { + return nil + } + return fmt.Errorf("mismatch") +} + +func (c singleMultilinLazyClaim) combinedSum(combinationCoeffs fr.Element) fr.Element { + return c.claimedSum +} + +func (c singleMultilinLazyClaim) degree(i int) int { + return 1 +} + +func (c singleMultilinLazyClaim) claimsNum() int { + return 1 +} + +func (c singleMultilinLazyClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} diff --git a/internal/gkr/bw6-633/test_vector_utils_test.go b/internal/gkr/bw6-633/test_vector_utils_test.go new file mode 100644 index 00000000..3857be16 --- /dev/null +++ b/internal/gkr/bw6-633/test_vector_utils_test.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "strings" + + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" + "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrtesting" +) + +func toElement(i int64) *fr.Element { + var res fr.Element + res.SetInt64(i) + return &res +} + +func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { + if _type, ok := d["type"]; ok { + switch _type { + case "const": + startState := int64(d["val"].(float64)) + return &messageCounter{startState: startState, step: 0, state: startState}, nil + default: + return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) + } + } + return nil, fmt.Errorf("hash description missing type") +} + +type messageCounter struct { + startState int64 + state int64 + step int64 +} + +func (m *messageCounter) Write(p []byte) (n int, err error) { + inputBlockSize := (len(p)-1)/fr.Bytes + 1 + m.state += int64(inputBlockSize) * m.step + return len(p), nil +} + +func (m *messageCounter) Sum(b []byte) []byte { + inputBlockSize := (len(b)-1)/fr.Bytes + 1 + resI := m.state + int64(inputBlockSize)*m.step + var res fr.Element + res.SetInt64(int64(resI)) + resBytes := res.Bytes() + return resBytes[:] +} + +func (m *messageCounter) Reset() { + m.state = m.startState +} + +func (m *messageCounter) Size() int { + return fr.Bytes +} + +func (m *messageCounter) BlockSize() int { + return fr.Bytes +} + +func newMessageCounter(startState, step int) hash.Hash { + transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} + return transcript +} + +func newMessageCounterGenerator(startState, step int) func() hash.Hash { + return func() hash.Hash { + return newMessageCounter(startState, step) + } +} + +func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { + + // TODO: Put this in element.SetString? + switch v := value.(type) { + case string: + + if sep := strings.Split(v, "/"); len(sep) == 2 { + var denom fr.Element + if _, err := z.SetString(sep[0]); err != nil { + return nil, err + } + if _, err := denom.SetString(sep[1]); err != nil { + return nil, err + } + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil + } + + case float64: + asInt := int64(v) + if float64(asInt) != v { + return nil, fmt.Errorf("cannot currently parse float") + } + z.SetInt64(asInt) + return z, nil + } + + return z.SetInterface(value) +} + +func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { + elementSlice := make([]fr.Element, len(slice)) + for i, v := range slice { + if _, err := setElement(&elementSlice[i], v); err != nil { + return nil, err + } + } + return elementSlice, nil +} + +func sliceEquals(a []fr.Element, b []fr.Element) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if !a[i].Equal(&b[i]) { + return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) + } + } + return nil +} + +func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if err := sliceEquals(a[i], b[i]); err != nil { + return fmt.Errorf("at index %d: %w", i, err) + } + } + return nil +} diff --git a/internal/gkr/bw6-761/gate_testing.go b/internal/gkr/bw6-761/gate_testing.go new file mode 100644 index 00000000..6eda2ebe --- /dev/null +++ b/internal/gkr/bw6-761/gate_testing.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + + "slices" + + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/fft" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// IsGateFunctionAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func IsGateFunctionAdditive(f gkr.GateFunction, i, nbIn int) bool { + fWrapped := api.convertFunc(f) + + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(fr.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 := fWrapped(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := fWrapped(in...) + + y2.Sub(y2, y1) + y1.Sub(y1, y0) + + if !y2.Equal(y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 = fWrapped(in...) + + y1.Sub(y1, y0) + + return y1.Equal(y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f gateFunctionFr) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]fr.Element, nbIn) + consts := make(fr.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + domain := fft.NewDomain(degreeBound) + // evaluate p on the unit circle (first filling p with evaluations rather than coefficients) + x := fr.One() + for i := range p { + fIn[0] = x + for j := range consts { + fIn[j+1].Mul(&x, &consts[j]) + } + p[i].Set(f(fIn...)) + + x.Mul(&x, &domain.Generator) + } + + // obtain p's coefficients + domain.FFTInverse(p, fft.DIF) + fft.BitReverse(p) + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +// FindGateFunctionDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func FindGateFunctionDegree(f gkr.GateFunction, max, nbIn int) (int, error) { + fFr := api.convertFunc(f) + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := fFr.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, gkrtypes.ErrZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func VerifyGateFunctionDegree(f gkr.GateFunction, claimedDegree, nbIn int) error { + fFr := api.convertFunc(f) + if p := fFr.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return gkrtypes.ErrZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} diff --git a/internal/gkr/bw6-761/gkr.go b/internal/gkr/bw6-761/gkr.go new file mode 100644 index 00000000..f90f2811 --- /dev/null +++ b/internal/gkr/bw6-761/gkr.go @@ -0,0 +1,816 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "sync" + + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// The goal is to prove/verify evaluations of many instances of the same circuit + +// WireAssignment is assignment of values to the same wire across many instances of the circuit +type WireAssignment []polynomial.MultiLin + +type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) + +// eqTimesGateEvalSumcheckLazyClaims is a lazy claim for sumcheck (verifier side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the checking of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckLazyClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ), allegedly + manager *claimsManager // WARNING: Circular references +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) getWire() *gkrtypes.Wire { + return e.manager.wires[e.wireI] +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) claimsNum() int { + return len(e.evaluationPoints) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) varsNum() int { + return len(e.evaluationPoints[0]) +} + +// combinedSum returns ∑ᵢ aⁱ yᵢ +func (e *eqTimesGateEvalSumcheckLazyClaims) combinedSum(a fr.Element) fr.Element { + evalsAsPoly := polynomial.Polynomial(e.claimedEvaluations) + return evalsAsPoly.Eval(&a) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) degree(int) int { + return 1 + e.manager.wires[e.wireI].Gate.Degree() +} + +// verifyFinalEval finalizes the verification of w. +// The prover's claims w(xᵢ) = yᵢ have already been reduced to verifying +// ∑ cⁱ eq(xᵢ, r) w(r) = purportedValue. ( c is combinationCoeff ) +// Both purportedValue and the vector r have been randomized during the sumcheck protocol. +// By taking the w term out of the sum we get the equivalent claim that +// for E := ∑ eq(xᵢ, r), it must be that E w(r) = purportedValue. +// If w is an input wire, the verifier can directly check its evaluation at r. +// Otherwise, the prover makes claims about the evaluation of w's input wires, +// wᵢ, at r, to be verified later. +// The claims are communicated through the proof parameter. +// The verifier checks here if the claimed evaluations of wᵢ(r) are consistent with +// the main claim, by checking E w(wᵢ(r)...) = purportedValue. +func (e *eqTimesGateEvalSumcheckLazyClaims) verifyFinalEval(r []fr.Element, combinationCoeff, purportedValue fr.Element, uniqueInputEvaluations []fr.Element) error { + // the eq terms ( E ) + numClaims := len(e.evaluationPoints) + evaluation := polynomial.EvalEq(e.evaluationPoints[numClaims-1], r) + for i := numClaims - 2; i >= 0; i-- { + evaluation.Mul(&evaluation, &combinationCoeff) + eq := polynomial.EvalEq(e.evaluationPoints[i], r) + evaluation.Add(&evaluation, &eq) + } + + wire := e.manager.wires[e.wireI] + + // the w(...) term + var gateEvaluation fr.Element + if wire.IsInput() { // just compute w(r) + gateEvaluation = e.manager.assignment[e.wireI].Evaluate(r, e.manager.memPool) + } else { // proof contains the evaluations of the inputs, but avoids repetition in case multiple inputs come from the same wire + injection, injectionLeftInv := + e.manager.wires.ClaimPropagationInfo(e.wireI) + + if len(injection) != len(uniqueInputEvaluations) { + return fmt.Errorf("%d input wire evaluations given, %d expected", len(uniqueInputEvaluations), len(injection)) + } + + for uniqueI, i := range injection { // map from unique to all + e.manager.add(wire.Inputs[i], r, uniqueInputEvaluations[uniqueI]) + } + + inputEvaluations := make([]frontend.Variable, len(wire.Inputs)) + for i, uniqueI := range injectionLeftInv { // map from all to unique + inputEvaluations[i] = &uniqueInputEvaluations[uniqueI] + } + + gateEvaluation.Set(wire.Gate.Evaluate(api, inputEvaluations...).(*fr.Element)) + } + + evaluation.Mul(&evaluation, &gateEvaluation) + + if evaluation.Equal(&purportedValue) { + return nil + } + return errors.New("incompatible evaluations") +} + +// eqTimesGateEvalSumcheckClaims is a claim for sumcheck (prover side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the proving of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]fr.Element // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []fr.Element // yᵢ = w(xᵢ) + manager *claimsManager + + input []polynomial.MultiLin // input[i](h₁, ..., hₘ₋ⱼ) = wᵢ(r₁, r₂, ..., rⱼ₋₁, h₁, ..., hₘ₋ⱼ) + + eq polynomial.MultiLin // E := ∑ᵢ cⁱ eq(xᵢ, -) +} + +func (c *eqTimesGateEvalSumcheckClaims) getWire() *gkrtypes.Wire { + return c.manager.wires[c.wireI] +} + +// combine the multiple claims into one claim using a random combination (combinationCoeff or c). +// From the original multiple claims of w(xᵢ) = yᵢ, we get a single claim +// ∑ᵢ,ₕ cⁱ eq(xᵢ, h) w(h) = ∑ᵢ cⁱ yᵢ, where h iterates over the hypercube (circuit instances) and +// i iterates over the claims. +// Equivalently, we could say ∑ᵢ cⁱ yᵢ = ∑ₕ,ᵢ cⁱ eq(xᵢ, h) w(h) = ∑ₕ w(h) ∑ᵢ cⁱ eq(xᵢ, h). +// Thus if we initially compute E := ∑ᵢ cⁱ eq(xᵢ, -), our claim will find the simpler form +// ∑ᵢ cⁱ yᵢ = ∑ₕ w(h) E(h), where the sum-checked polynomial is of degree deg(g) + 1, +// and deg(g) is the total degree of the polynomial defining the gate g of which w is the output. +// The output of combine is the first sumcheck claim, i.e. ∑₍ₕ₁,ₕ₂,...₎ w(X, h₁, h₂, ...) E(X, h₁, h₂, ...).. +func (c *eqTimesGateEvalSumcheckClaims) combine(combinationCoeff fr.Element) polynomial.Polynomial { + varsNum := c.varsNum() + eqLength := 1 << varsNum + claimsNum := c.claimsNum() + // initialize the eq tables ( E ) + c.eq = c.manager.memPool.Make(eqLength) + + c.eq[0].SetOne() + c.eq.Eq(c.evaluationPoints[0]) + + // E := eq(x₀, -) + newEq := polynomial.MultiLin(c.manager.memPool.Make(eqLength)) + aI := combinationCoeff + + // E += cⁱ eq(xᵢ, -) + for k := 1; k < claimsNum; k++ { + newEq[0].Set(&aI) + + c.eqAcc(c.eq, newEq, c.evaluationPoints[k]) + + if k+1 < claimsNum { + aI.Mul(&aI, &combinationCoeff) + } + } + + c.manager.memPool.Dump(newEq) + + return c.computeGJ() +} + +// eqAcc sets m to an eq table at q and then adds it to e. +// m <- eq(q, -). +// e <- e + m +func (c *eqTimesGateEvalSumcheckClaims) eqAcc(e, m polynomial.MultiLin, q []fr.Element) { + n := len(q) + + //At the end of each iteration, m(h₁, ..., hₙ) = eq(q₁, ..., qᵢ₊₁, h₁, ..., hᵢ₊₁) + for i := range q { // In the comments we use a 1-based index so q[i] = qᵢ₊₁ + // go through all assignments of (b₁, ..., bᵢ) ∈ {0,1}ⁱ + const threshold = 1 << 6 + k := 1 << i + if k < threshold { + for j := 0; j < k; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + } else { + c.manager.workers.Submit(k, func(start, end int) { + for j := start; j < end; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + }, 1024).Wait() + } + + } + c.manager.workers.Submit(len(e), func(start, end int) { + for i := start; i < end; i++ { + e[i].Add(&e[i], &m[i]) + } + }, 512).Wait() +} + +// computeGJ: gⱼ = ∑_{0≤h<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, h...) = ∑_{0≤i<2ⁿ⁻ʲ} E(r₁, ..., Xⱼ, h...) g( w₀(r₁, ..., Xⱼ, h...), ... ). +// the polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). +// The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). By convention, g₀ is a constant polynomial equal to the claimed sum. +func (c *eqTimesGateEvalSumcheckClaims) computeGJ() polynomial.Polynomial { + + wire := c.getWire() + degGJ := 1 + wire.Gate.Degree() // guaranteed to be no smaller than the actual deg(gⱼ) + nbGateIn := len(c.input) + + // Both E and wᵢ (the input wires and the eq table) are multilinear, thus + // they are linear in Xⱼ. + // So for f ∈ { E(r₁, ..., Xⱼ, h...) } ∪ {wᵢ(r₁, ..., Xⱼ, h...) }, so f(m) = m×(f(1) - f(0)) + f(0), and f(0), f(1) are easily computed from the bookkeeping tables. + // ml are such multilinear polynomials the evaluations of which over different values of Xⱼ are computed in this stepwise manner. + ml := make([]polynomial.MultiLin, nbGateIn+1) // shortcut to the evaluations of the multilinear polynomials over the hypercube + ml[0] = c.eq + copy(ml[1:], c.input) + + sumSize := len(c.eq) / 2 // the range of h, over which we sum + + // Perf-TODO: Collate once at claim "combination" time and not again. then, even folding can be done in one operation every time "next" is called + + gJ := make([]fr.Element, degGJ) + var mu sync.Mutex + computeAll := func(start, end int) { // compute method to allow parallelization across instances + var step fr.Element + + res := make([]fr.Element, degGJ) + + // evaluations of ml, laid out as: + // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), + // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), + // ... + // ml[0](degGJ, h...), ml[2](degGJ, h...), ..., ml[len(ml)-1](degGJ, h...) + mlEvals := make([]fr.Element, degGJ*len(ml)) + gateInput := make([]frontend.Variable, nbGateIn) + + for h := start; h < end; h++ { // h counts across instances + + evalAt1Index := sumSize + h + for k := range ml { + // d = 0 + mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1. Can be taken directly from the table. + step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for d := 1; d < degGJ; d++ { + mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + } + } + + eIndex := 0 // index for where the current eq term is + nextEIndex := len(ml) + for d := range degGJ { + for i := range gateInput { + gateInput[i] = &mlEvals[eIndex+1+i] + } + summand := wire.Gate.Evaluate(api, gateInput...).(*fr.Element) + summand.Mul(summand, &mlEvals[eIndex]) + res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + } + } + mu.Lock() + for i := range gJ { + gJ[i].Add(&gJ[i], &res[i]) // collect into the complete sum + } + mu.Unlock() + } + + const minBlockSize = 64 + + if sumSize < minBlockSize { + // no parallelization + computeAll(0, sumSize) + } else { + c.manager.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } + + return gJ +} + +// next first folds the input and E polynomials at the given verifier challenge then computes the new gⱼ. +// Thus, j <- j+1 and rⱼ = challenge. +func (c *eqTimesGateEvalSumcheckClaims) next(challenge fr.Element) polynomial.Polynomial { + const minBlockSize = 512 + n := len(c.eq) / 2 + if n < minBlockSize { + // no parallelization + for i := 0; i < len(c.input); i++ { + c.input[i].Fold(challenge) + } + c.eq.Fold(challenge) + } else { + wgs := make([]*sync.WaitGroup, len(c.input)) + for i := 0; i < len(c.input); i++ { + wgs[i] = c.manager.workers.Submit(n, c.input[i].FoldParallel(challenge), minBlockSize) + } + c.manager.workers.Submit(n, c.eq.FoldParallel(challenge), minBlockSize).Wait() + for _, wg := range wgs { + wg.Wait() + } + } + + return c.computeGJ() +} + +func (c *eqTimesGateEvalSumcheckClaims) varsNum() int { + return len(c.evaluationPoints[0]) +} + +func (c *eqTimesGateEvalSumcheckClaims) claimsNum() int { + return len(c.claimedEvaluations) +} + +// proveFinalEval provides the values wᵢ(r₁, ..., rₙ) +func (c *eqTimesGateEvalSumcheckClaims) proveFinalEval(r []fr.Element) []fr.Element { + //defer the proof, return list of claims + + injection, _ := c.manager.wires.ClaimPropagationInfo(c.wireI) // TODO @Tabaie: Instead of doing this last, we could just have fewer input in the first place; not that likely to happen with single gates, but more so with layers. + evaluations := make([]fr.Element, len(injection)) + for i, gateInputI := range injection { + wI := c.input[gateInputI] + wI.Fold(r[len(r)-1]) // We already have wᵢ(r₁, ..., rₙ₋₁, hₙ) in a table. Only one more fold required. + c.manager.add(c.getWire().Inputs[gateInputI], r, wI[0]) + evaluations[i] = wI[0] + } + + c.manager.memPool.Dump(c.claimedEvaluations, c.eq) + + return evaluations +} + +type claimsManager struct { + claims []*eqTimesGateEvalSumcheckLazyClaims + assignment WireAssignment + memPool *polynomial.Pool + workers *utils.WorkerPool + wires gkrtypes.Wires +} + +func newClaimsManager(wires []*gkrtypes.Wire, assignment WireAssignment, o settings) (manager claimsManager) { + manager.assignment = assignment + manager.claims = make([]*eqTimesGateEvalSumcheckLazyClaims, len(wires)) + manager.memPool = o.pool + manager.workers = o.workers + manager.wires = wires + + for i, wire := range wires { + + manager.claims[i] = &eqTimesGateEvalSumcheckLazyClaims{ + wireI: i, + evaluationPoints: make([][]fr.Element, 0, wire.NbClaims()), + claimedEvaluations: manager.memPool.Make(wire.NbClaims()), + manager: &manager, + } + } + return +} + +func (m *claimsManager) add(wire int, evaluationPoint []fr.Element, evaluation fr.Element) { + claim := m.claims[wire] + i := len(claim.evaluationPoints) + claim.claimedEvaluations[i] = evaluation + claim.evaluationPoints = append(claim.evaluationPoints, evaluationPoint) +} + +func (m *claimsManager) getLazyClaim(wire int) *eqTimesGateEvalSumcheckLazyClaims { + return m.claims[wire] +} + +func (m *claimsManager) getClaim(wireI int) *eqTimesGateEvalSumcheckClaims { + lazy := m.claims[wireI] + wire := m.wires[wireI] + res := &eqTimesGateEvalSumcheckClaims{ + wireI: wireI, + evaluationPoints: lazy.evaluationPoints, + claimedEvaluations: lazy.claimedEvaluations, + manager: m, + } + + if wire.IsInput() { + res.input = []polynomial.MultiLin{m.memPool.Clone(m.assignment[wireI])} + } else { + res.input = make([]polynomial.MultiLin, len(wire.Inputs)) + + for inputI, inputW := range wire.Inputs { + res.input[inputI] = m.memPool.Clone(m.assignment[inputW]) //will be edited later, so must be deep copied + } + } + return res +} + +func (m *claimsManager) deleteClaim(wire int) { + m.claims[wire].manager = nil + m.claims[wire] = nil +} + +type settings struct { + pool *polynomial.Pool + sorted []*gkrtypes.Wire + transcript *fiatshamir.Transcript + transcriptPrefix string + nbVars int + workers *utils.WorkerPool +} + +type Option func(*settings) + +func WithPool(pool *polynomial.Pool) Option { + return func(options *settings) { + options.pool = pool + } +} + +func WithSortedCircuit(sorted []*gkrtypes.Wire) Option { + return func(options *settings) { + options.sorted = sorted + } +} + +func WithWorkers(workers *utils.WorkerPool) Option { + return func(options *settings) { + options.workers = workers + } +} + +func setup(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (settings, error) { + var o settings + var err error + for _, option := range options { + option(&o) + } + + o.nbVars = assignment.NumVars() + nbInstances := assignment.NumInstances() + if 1< 1 { //combine the claims + size++ + } + size += logNbInstances // full run of sumcheck on logNbInstances variables + } + + nums := make([]string, max(len(sorted), logNbInstances)) + for i := range nums { + nums[i] = strconv.Itoa(i) + } + + challenges := make([]string, size) + + // output wire claims + firstChallengePrefix := prefix + "fC." + for j := 0; j < logNbInstances; j++ { + challenges[j] = firstChallengePrefix + nums[j] + } + j := logNbInstances + for i := len(sorted) - 1; i >= 0; i-- { + if sorted[i].NoProof() { + continue + } + wirePrefix := prefix + "w" + nums[i] + "." + + if sorted[i].NbClaims() > 1 { + challenges[j] = wirePrefix + "comb" + j++ + } + + partialSumPrefix := wirePrefix + "pSP." + for k := 0; k < logNbInstances; k++ { + challenges[j] = partialSumPrefix + nums[k] + j++ + } + } + return challenges +} + +func getFirstChallengeNames(logNbInstances int, prefix string) []string { + res := make([]string, logNbInstances) + firstChallengePrefix := prefix + "fC." + for i := 0; i < logNbInstances; i++ { + res[i] = firstChallengePrefix + strconv.Itoa(i) + } + return res +} + +func getChallenges(transcript *fiatshamir.Transcript, names []string) ([]fr.Element, error) { + res := make([]fr.Element, len(names)) + for i, name := range names { + if bytes, err := transcript.ComputeChallenge(name); err == nil { + res[i].SetBytes(bytes) + } else { + return nil, err + } + } + return res, nil +} + +// Prove consistency of the claimed assignment +func Prove(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (Proof, error) { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return nil, err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + proof := make(Proof, len(c)) + // firstChallenge called rho in the paper + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return nil, err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + claim := claims.getClaim(i) + if wire.NoProof() { // input wires with one claim only + proof[i] = sumcheckProof{ + partialSumPolys: []polynomial.Polynomial{}, + finalEvalProof: []fr.Element{}, + } + } else { + if proof[i], err = sumcheckProve( + claim, fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err != nil { + return proof, err + } + + baseChallenge = make([][]byte, len(proof[i].finalEvalProof)) + for j := range proof[i].finalEvalProof { + baseChallenge[j] = proof[i].finalEvalProof[j].Marshal() + } + } + // the verifier checks a single claim about input wires itself + claims.deleteClaim(i) + } + + return proof, nil +} + +// Verify the consistency of the claimed output with the claimed input +// Unlike in Prove, the assignment argument need not be complete +func Verify(c gkrtypes.Circuit, assignment WireAssignment, proof Proof, transcriptSettings fiatshamir.Settings, options ...Option) error { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + var firstChallenge []fr.Element + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + proofW := proof[i] + claim := claims.getLazyClaim(i) + if wire.NoProof() { // input wires with one claim only + // make sure the proof is empty + if len(proofW.finalEvalProof) != 0 || len(proofW.partialSumPolys) != 0 { + return errors.New("no proof allowed for input wire with a single claim") + } + + if wire.NbClaims() == 1 { // input wire + // simply evaluate and see if it matches + evaluation := assignment[i].Evaluate(claim.evaluationPoints[0], claims.memPool) + if !claim.claimedEvaluations[0].Equal(&evaluation) { + return errors.New("incorrect input wire claim") + } + } + } else if err = sumcheckVerify( + claim, proof[i], fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err == nil { // incorporate prover claims about w's input into the transcript + baseChallenge = make([][]byte, len(proofW.finalEvalProof)) + for j := range baseChallenge { + baseChallenge[j] = proofW.finalEvalProof[j].Marshal() + } + } else { + return fmt.Errorf("sumcheck proof rejected: %v", err) //TODO: Any polynomials to dump? + } + claims.deleteClaim(i) + } + return nil +} + +// Complete the circuit evaluation from input values +func (a WireAssignment) Complete(wires gkrtypes.Wires) WireAssignment { + + nbInstances := a.NumInstances() + maxNbIns := 0 + + for i, w := range wires { + maxNbIns = max(maxNbIns, len(w.Inputs)) + if len(a[i]) != nbInstances { + a[i] = make([]fr.Element, nbInstances) + } + } + + ins := make([]fr.Element, maxNbIns) + for i := range nbInstances { + for wI, w := range wires { + if !w.IsInput() { + for inI, in := range w.Inputs { + ins[inI] = a[in][i] + } + a[wI][i].Set(api.evaluate(w.Gate.Evaluate, ins[:len(w.Inputs)]...)) + } + } + } + + return a +} + +func (a WireAssignment) NumInstances() int { + for _, aW := range a { + return len(aW) + } + panic("empty assignment") +} + +func (a WireAssignment) NumVars() int { + for _, aW := range a { + return aW.NumVars() + } + panic("empty assignment") +} + +// SerializeToBigInts flattens a proof object into the given slice of big.Ints +// useful in gnark hints. +func (p Proof) SerializeToBigInts(outs []*big.Int) error { + offset := 0 + for i := range p { + for _, poly := range p[i].partialSumPolys { + frToBigInts(outs[offset:], poly) + offset += len(poly) + } + if p[i].finalEvalProof != nil { + frToBigInts(outs[offset:], p[i].finalEvalProof) + offset += len(p[i].finalEvalProof) + } + } + if offset != len(outs) { + return fmt.Errorf("expected %d elements, got %d", offset, len(outs)) + } + return nil +} + +func frToBigInts(dst []*big.Int, src []fr.Element) { + for i := range src { + src[i].BigInt(dst[i]) + } +} + +// gateAPI implements gkr.GateAPI. +type gateAPI struct{} + +var api gateAPI + +func (gateAPI) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element // TODO Heap allocated. Keep an eye on perf + res.Add(cast(i1), cast(i2)) + for _, v := range in { + res.Add(&res, cast(v)) + } + return &res +} + +func (gateAPI) MulAcc(a, b, c frontend.Variable) frontend.Variable { + var prod fr.Element + prod.Add(cast(b), cast(c)) + res := cast(a) + res.Add(res, &prod) + return &res +} + +func (gateAPI) Neg(i1 frontend.Variable) frontend.Variable { + var res fr.Element + res.Neg(cast(i1)) + return &res +} + +func (gateAPI) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Sub(cast(i1), cast(i2)) + for _, v := range in { + res.Sub(&res, cast(v)) + } + return &res +} + +func (gateAPI) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res fr.Element + res.Mul(cast(i1), cast(i2)) + for _, v := range in { + res.Mul(&res, cast(v)) + } + return &res +} + +func (gateAPI) Println(a ...frontend.Variable) { + toPrint := make([]any, len(a)) + var x fr.Element + + for i, v := range a { + if _, err := x.SetInterface(v); err != nil { + toPrint[i] = x.String() + } else { + if s, ok := v.(string); ok { + toPrint[i] = s + continue + } + panic(fmt.Errorf("not numeric or string: %w", err)) + } + } + fmt.Println(toPrint...) +} + +func (api gateAPI) evaluate(f gkr.GateFunction, in ...fr.Element) *fr.Element { + inVar := make([]frontend.Variable, len(in)) + for i := range in { + inVar[i] = &in[i] + } + return f(api, inVar...).(*fr.Element) +} + +type gateFunctionFr func(...fr.Element) *fr.Element + +// convertFunc turns f into a function that accepts and returns fr.Element. +func (api gateAPI) convertFunc(f gkr.GateFunction) gateFunctionFr { + return func(in ...fr.Element) *fr.Element { + return api.evaluate(f, in...) + } +} + +func cast(v frontend.Variable) *fr.Element { + if x, ok := v.(*fr.Element); ok { // fast path, no extra heap allocation + return x + } + var x fr.Element + if _, err := x.SetInterface(v); err != nil { + panic(err) + } + return &x +} diff --git a/internal/gkr/bw6-761/gkr_test.go b/internal/gkr/bw6-761/gkr_test.go new file mode 100644 index 00000000..185ca303 --- /dev/null +++ b/internal/gkr/bw6-761/gkr_test.go @@ -0,0 +1,505 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "os" + "path/filepath" + "reflect" + "strconv" + "testing" + "time" + + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/mimc" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + gcUtils "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/internal/utils" + "github.com/consensys/gnark/std/gkrapi/gkr" + "github.com/stretchr/testify/assert" +) + +func TestNoGateTwoInstances(t *testing.T) { + // Testing a single instance is not possible because the sumcheck implementation doesn't cover the trivial 0-variate case + testNoGate(t, []fr.Element{four, three}) +} + +func TestNoGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}}) +} + +func TestSingleAddGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Add2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleMulGate(t *testing.T) { + test(t, gkrtypes.Circuit{{}, {}, { + Gate: gkrtypes.Mul2(), + Inputs: []int{0, 1}, + }}) +} + +func TestSingleInputTwoIdentityGates(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + }) +} + +func TestSingleInputTwoIdentityGatesComposed(t *testing.T) { + test(t, gkrtypes.Circuit{{}, + { + Gate: gkrtypes.Identity(), + Inputs: []int{0}, + }, + { + Gate: gkrtypes.Identity(), + Inputs: []int{1}, + }}) +} + +func TestAPowNTimesBCircuit(t *testing.T) { + const N = 10 + + c := make(gkrtypes.Circuit, N+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: gkrtypes.Mul2(), + Inputs: []int{i - 1, 0}, + } + } + + test(t, c) +} + +func TestSingleMimcCipherGate(t *testing.T) { + test(t, gkrtypes.Circuit{ + {}, {}, + { + Inputs: []int{0, 1}, + Gate: cache.GetGate("mimc"), + }, + }) +} + +func TestShallowMimcTwoInstances(t *testing.T) { + test(t, mimcCircuit(2)) +} + +func TestMimc(t *testing.T) { + test(t, mimcCircuit(93)) +} + +func TestSumcheckFromSingleInputTwoIdentityGatesGateTwoInstances(t *testing.T) { + circuit := gkrtypes.Circuit{gkrtypes.Wire{ + Gate: gkrtypes.Identity(), + NbUniqueOutputs: 2, + }} + + assignment := WireAssignment{[]fr.Element{two, three}} + var o settings + pool := polynomial.NewPool(256, 1<<11) + workers := gcUtils.NewWorkerPool() + o.pool = &pool + o.workers = workers + + claimsManagerGen := func() *claimsManager { + manager := newClaimsManager(utils.References(circuit), assignment, o) + manager.add(0, []fr.Element{three}, five) + manager.add(0, []fr.Element{four}, six) + return &manager + } + + transcriptGen := newMessageCounterGenerator(4, 1) + + proof, err := sumcheckProve(claimsManagerGen().getClaim(0), fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) + err = sumcheckVerify(claimsManagerGen().getLazyClaim(0), proof, fiatshamir.WithHash(transcriptGen(), nil)) + assert.NoError(t, err) +} + +var one, two, three, four, five, six fr.Element + +func init() { + one.SetOne() + two.Double(&one) + three.Add(&two, &one) + four.Double(&two) + five.Add(&three, &two) + six.Double(&three) +} + +var testManyInstancesLogMaxInstances = -1 + +func getLogMaxInstances(t *testing.T) int { + if testManyInstancesLogMaxInstances == -1 { + + s := os.Getenv("GKR_LOG_INSTANCES") + if s == "" { + testManyInstancesLogMaxInstances = 5 + } else { + var err error + testManyInstancesLogMaxInstances, err = strconv.Atoi(s) + if err != nil { + t.Error(err) + } + } + + } + return testManyInstancesLogMaxInstances +} + +func test(t *testing.T, circuit gkrtypes.Circuit) { + wireRefs := utils.References(circuit) + ins := circuit.Inputs() + insAssignment := make(WireAssignment, len(ins)) + maxSize := 1 << getLogMaxInstances(t) + + for i := range ins { + insAssignment[i] = make([]fr.Element, maxSize) + fr.Vector(insAssignment[i]).MustSetRandom() + } + + fullAssignment := make(WireAssignment, len(circuit)) + for _, numEvals := range []int{2, maxSize} { + for i := range ins { + fullAssignment[ins[i]] = insAssignment[i][:numEvals] + } + + fullAssignment.Complete(wireRefs) + + t.Log("Selected inputs for test") + + proof, err := Prove(circuit, fullAssignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") + + if proof.isEmpty() { // special case for TestNoGate: + continue // there's no way to make a trivial proof fail + } + + err = Verify(circuit, fullAssignment, proof, fiatshamir.WithHash(newMessageCounter(0, 1))) + assert.NotNil(t, err, "bad proof accepted") + } + +} + +func (p Proof) isEmpty() bool { + for i := range p { + if len(p[i].finalEvalProof) != 0 { + return false + } + for j := range p[i].partialSumPolys { + if len(p[i].partialSumPolys[j]) != 0 { + return false + } + } + } + return true +} + +func testNoGate(t *testing.T, inputAssignments ...[]fr.Element) { + c := gkrtypes.Circuit{ + {}, + } + + assignment := WireAssignment{0: inputAssignments[0]} + + proof, err := Prove(c, assignment, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err) + + // Even though a hash is called here, the proof is empty + + err = Verify(c, assignment, proof, fiatshamir.WithHash(newMessageCounter(1, 1))) + assert.NoError(t, err, "proof rejected") +} + +func mimcCircuit(numRounds int) gkrtypes.Circuit { + c := make(gkrtypes.Circuit, numRounds+2) + + for i := 2; i < len(c); i++ { + c[i] = gkrtypes.Wire{ + Gate: cache.GetGate("mimc"), + Inputs: []int{i - 1, 0}, + } + } + return c +} + +func TestIsAdditive(t *testing.T) { + + // f: x,y -> x² + xy + f := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("bivariate input needed") + } + res := api.Add(x[0], x[1]) + return api.Mul(res, x[0]) + } + + // g: x,y -> x² + 3y + g := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + res := api.Mul(x[0], x[0]) + y3 := api.Mul(x[1], 3) + return api.Add(res, y3) + } + + // h: x -> 2x + // but it edits it input + h := func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + return api.Add(x[0], x[0]) + } + + assert.False(t, IsGateFunctionAdditive(f, 1, 2)) + assert.False(t, IsGateFunctionAdditive(f, 0, 2)) + + assert.False(t, IsGateFunctionAdditive(g, 0, 2)) + assert.True(t, IsGateFunctionAdditive(g, 1, 2)) + + assert.True(t, IsGateFunctionAdditive(h, 0, 1)) +} + +func generateTestProver(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + proof, err := Prove(testCase.Circuit, testCase.FullAssignment, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err) + assert.NoError(t, proofEquals(testCase.Proof, proof)) + } +} + +func generateTestVerifier(path string) func(t *testing.T) { + return func(t *testing.T) { + testCase, err := newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(testCase.Hash)) + assert.NoError(t, err, "proof rejected") + testCase, err = newTestCase(path) + assert.NoError(t, err) + err = Verify(testCase.Circuit, testCase.InOutAssignment, testCase.Proof, fiatshamir.WithHash(newMessageCounter(2, 0))) + assert.NotNil(t, err, "bad proof accepted") + } +} + +func TestGkrVectors(t *testing.T) { + + const testDirPath = "../test_vectors/" + dirEntries, err := os.ReadDir(testDirPath) + assert.NoError(t, err) + for _, dirEntry := range dirEntries { + if !dirEntry.IsDir() { + + if filepath.Ext(dirEntry.Name()) == ".json" { + path := filepath.Join(testDirPath, dirEntry.Name()) + noExt := dirEntry.Name()[:len(dirEntry.Name())-len(".json")] + + t.Run(noExt+"_prover", generateTestProver(path)) + t.Run(noExt+"_verifier", generateTestVerifier(path)) + + } + } + } +} + +func proofEquals(expected Proof, seen Proof) error { + if len(expected) != len(seen) { + return fmt.Errorf("length mismatch %d ≠ %d", len(expected), len(seen)) + } + for i, x := range expected { + xSeen := seen[i] + + if xSeen.finalEvalProof == nil { + if seenFinalEval := x.finalEvalProof; len(seenFinalEval) != 0 { + return fmt.Errorf("length mismatch %d ≠ %d", 0, len(seenFinalEval)) + } + } else { + if err := sliceEquals(x.finalEvalProof, xSeen.finalEvalProof); err != nil { + return fmt.Errorf("final evaluation proof mismatch") + } + } + if err := polynomialSliceEquals(x.partialSumPolys, xSeen.partialSumPolys); err != nil { + return err + } + } + return nil +} + +func benchmarkGkrMiMC(b *testing.B, nbInstances, mimcDepth int) { + fmt.Println("creating circuit structure") + c := mimcCircuit(mimcDepth) + + in0 := make([]fr.Element, nbInstances) + in1 := make([]fr.Element, nbInstances) + fr.Vector(in0).MustSetRandom() + fr.Vector(in1).MustSetRandom() + + fmt.Println("evaluating circuit") + start := time.Now().UnixMicro() + assignment := WireAssignment{in0, in1}.Complete(utils.References(c)) + solved := time.Now().UnixMicro() - start + fmt.Println("solved in", solved, "μs") + + //b.ResetTimer() + fmt.Println("constructing proof") + start = time.Now().UnixMicro() + _, err := Prove(c, assignment, fiatshamir.WithHash(mimc.NewMiMC())) + proved := time.Now().UnixMicro() - start + fmt.Println("proved in", proved, "μs") + assert.NoError(b, err) +} + +func BenchmarkGkrMimc19(b *testing.B) { + benchmarkGkrMiMC(b, 1<<19, 91) +} + +func BenchmarkGkrMimc17(b *testing.B) { + benchmarkGkrMiMC(b, 1<<17, 91) +} + +func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { + proof := make(Proof, len(printable)) + for i := range printable { + finalEvalProof := []fr.Element(nil) + + if printable[i].FinalEvalProof != nil { + finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) + finalEvalProof = make([]fr.Element, finalEvalSlice.Len()) + for k := range finalEvalProof { + if _, err := setElement(&finalEvalProof[k], finalEvalSlice.Index(k).Interface()); err != nil { + return nil, err + } + } + } + + proof[i] = sumcheckProof{ + partialSumPolys: make([]polynomial.Polynomial, len(printable[i].PartialSumPolys)), + finalEvalProof: finalEvalProof, + } + for k := range printable[i].PartialSumPolys { + var err error + if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + return nil, err + } + } + } + return proof, nil +} + +type TestCase struct { + Circuit gkrtypes.Circuit + Hash hash.Hash + Proof Proof + FullAssignment WireAssignment + InOutAssignment WireAssignment +} + +var ( + testCases = make(map[string]*TestCase) + cache = gkrtesting.NewCache() +) + +func newTestCase(path string) (*TestCase, error) { + path, err := filepath.Abs(path) + if err != nil { + return nil, err + } + dir := filepath.Dir(path) + + tCase, ok := testCases[path] + if ok { + return tCase, nil + } + + info, err := cache.ReadTestCaseInfo(path) + if err != nil { + return nil, err + } + + circuit := cache.GetCircuit(filepath.Join(dir, info.Circuit)) + var _hash hash.Hash + if _hash, err = hashFromDescription(info.Hash); err != nil { + return nil, err + } + var proof Proof + if proof, err = unmarshalProof(info.Proof); err != nil { + return nil, err + } + + fullAssignment := make(WireAssignment, len(circuit)) + inOutAssignment := make(WireAssignment, len(circuit)) + + sorted := circuit.TopologicalSort() + + inI, outI := 0, 0 + for i, w := range sorted { + var assignmentRaw []interface{} + if w.IsInput() { + if inI == len(info.Input) { + return nil, fmt.Errorf("fewer input in vector than in circuit") + } + assignmentRaw = info.Input[inI] + inI++ + } else if w.IsOutput() { + if outI == len(info.Output) { + return nil, fmt.Errorf("fewer output in vector than in circuit") + } + assignmentRaw = info.Output[outI] + outI++ + } + if assignmentRaw != nil { + var wireAssignment []fr.Element + if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + return nil, err + } + + fullAssignment[i] = wireAssignment + inOutAssignment[i] = wireAssignment + } + } + + fullAssignment.Complete(utils.References(circuit)) + + for i, w := range sorted { + if w.IsOutput() { + if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + return nil, fmt.Errorf("assignment mismatch: %v", err) + } + } + } + + tCase = &TestCase{ + FullAssignment: fullAssignment, + InOutAssignment: inOutAssignment, + Proof: proof, + Hash: _hash, + Circuit: circuit, + } + + testCases[path] = tCase + + return tCase, nil +} diff --git a/internal/gkr/bw6-761/solver_hints.go b/internal/gkr/bw6-761/solver_hints.go new file mode 100644 index 00000000..606f13ec --- /dev/null +++ b/internal/gkr/bw6-761/solver_hints.go @@ -0,0 +1,146 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark-crypto/hash" + "github.com/consensys/gnark-crypto/utils" + hint "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + algo_utils "github.com/consensys/gnark/internal/utils" + + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +type SolvingData struct { + assignment WireAssignment + circuit gkrtypes.Circuit + workers *utils.WorkerPool +} + +func (d *SolvingData) init(info gkrtypes.SolvingInfo) { + d.workers = utils.NewWorkerPool() + d.circuit = info.Circuit + d.circuit.SetNbUniqueOutputs() + + d.assignment = make(WireAssignment, len(d.circuit)) + for i := range d.assignment { + d.assignment[i] = make([]fr.Element, info.NbInstances) + } +} + +// this module assumes that wire and instance indexes respect dependencies + +func setOuts(a WireAssignment, circuit gkrtypes.Circuit, outs []*big.Int) { + outsI := 0 + for i := range circuit { + if circuit[i].IsOutput() { + for j := range a[i] { + a[i][j].BigInt(outs[outsI]) + outsI++ + } + } + } + // Check if outsI == len(outs)? +} + +func SolveHint(info gkrtypes.SolvingInfo, data *SolvingData) hint.Hint { + return func(_ *big.Int, ins, outs []*big.Int) error { + // assumes assignmentVector is arranged wire first, instance second in order of solution + offsets := info.AssignmentOffsets() + data.init(info) + maxNIn := data.circuit.MaxGateNbIn() + + chunks := info.Chunks() + + solveTask := func(chunkOffset int) utils.Task { + return func(startInChunk, endInChunk int) { + start := startInChunk + chunkOffset + end := endInChunk + chunkOffset + inputs := make([]frontend.Variable, maxNIn) + dependencyHeads := make([]int, len(data.circuit)) // for each wire, which of its dependencies we would look at next + for wI := range data.circuit { // skip instances that are not relevant (related to instances before the current task) + deps := info.Dependencies[wI] + dependencyHeads[wI] = algo_utils.BinarySearchFunc(func(i int) int { + return deps[i].InputInstance + }, len(deps), start) + } + + for instanceI := start; instanceI < end; instanceI++ { + for wireI := range data.circuit { + wire := &data.circuit[wireI] + deps := info.Dependencies[wireI] + if wire.IsInput() { + if dependencyHeads[wireI] < len(deps) && instanceI == deps[dependencyHeads[wireI]].InputInstance { + dep := deps[dependencyHeads[wireI]] + data.assignment[wireI][instanceI].Set(&data.assignment[dep.OutputWire][dep.OutputInstance]) + dependencyHeads[wireI]++ + } else { + data.assignment[wireI][instanceI].SetBigInt(ins[offsets[wireI]+instanceI-dependencyHeads[wireI]]) + } + } else { + // assemble the inputs + inputIndexes := info.Circuit[wireI].Inputs + for i, inputI := range inputIndexes { + inputs[i] = &data.assignment[inputI][instanceI] + } + gate := data.circuit[wireI].Gate + data.assignment[wireI][instanceI].Set(gate.Evaluate(api, inputs[:len(inputIndexes)]...).(*fr.Element)) + } + } + } + } + } + + start := 0 + for _, end := range chunks { + data.workers.Submit(end-start, solveTask(start), 1024).Wait() + start = end + } + + for _, p := range info.Prints { + serializable := make([]any, len(p.Values)) + for i, v := range p.Values { + if p.IsGkrVar[i] { // serializer stores uint32 in slices as uint64 + serializable[i] = data.assignment[algo_utils.ForceUint32(v)][p.Instance].String() + } else { + serializable[i] = v + } + } + fmt.Println(serializable...) + } + + setOuts(data.assignment, info.Circuit, outs) + + return nil + } +} + +func ProveHint(hashName string, data *SolvingData) hint.Hint { + + return func(_ *big.Int, ins, outs []*big.Int) error { + insBytes := algo_utils.Map(ins[1:], func(i *big.Int) []byte { // the first input is dummy, just to ensure the solver's work is done before the prover is called + b := make([]byte, fr.Bytes) + i.FillBytes(b) + return b[:] + }) + + hsh := hash.NewHash(hashName + "_BW6_761") + + proof, err := Prove(data.circuit, data.assignment, fiatshamir.WithHash(hsh, insBytes...), WithWorkers(data.workers)) + if err != nil { + return err + } + + return proof.SerializeToBigInts(outs) + + } +} diff --git a/internal/gkr/bw6-761/sumcheck.go b/internal/gkr/bw6-761/sumcheck.go new file mode 100644 index 00000000..44591ffd --- /dev/null +++ b/internal/gkr/bw6-761/sumcheck.go @@ -0,0 +1,171 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "strconv" + + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" +) + +// This does not make use of parallelism and represents polynomials as lists of coefficients +// It is currently geared towards arithmetic hashes. Once we have a more unified hash function interface, this can be generified. + +// sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. +// Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) +type sumcheckClaims interface { + combine(a fr.Element) polynomial.Polynomial // combine into the 0ᵗʰ sumcheck subclaim. Create g := ∑_{1≤j≤m} aʲ⁻¹fⱼ for which now we seek to prove ∑_{0≤i<2ⁿ} g(i) = c := ∑_{1≤j≤m} aʲ⁻¹cⱼ. Return g₁. + next(fr.Element) polynomial.Polynomial // Return the evaluations gⱼ(k) for 1 ≤ k < degⱼ(g). Update the claim to gⱼ₊₁ for the input value as rⱼ + varsNum() int // number of variables + claimsNum() int // number of claims + proveFinalEval(r []fr.Element) []fr.Element // in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +// sumcheckLazyClaims is the sumcheckClaims data structure on the verifier side. It is "lazy" in that it has to compute fewer things. +type sumcheckLazyClaims interface { + claimsNum() int // claimsNum = m + varsNum() int // varsNum = n + combinedSum(a fr.Element) fr.Element // combinedSum returns c = ∑_{1≤j≤m} aʲ⁻¹cⱼ + degree(i int) int // degree of the total claim in the i'th variable + verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error +} + +// sumcheckProof of a multi-statement. +type sumcheckProof struct { + partialSumPolys []polynomial.Polynomial + finalEvalProof []fr.Element //in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +func setupTranscript(claimsNum int, varsNum int, settings *fiatshamir.Settings) (challengeNames []string, err error) { + numChallenges := varsNum + if claimsNum >= 2 { + numChallenges++ + } + challengeNames = make([]string, numChallenges) + if claimsNum >= 2 { + challengeNames[0] = settings.Prefix + "comb" + } + prefix := settings.Prefix + "pSP." + for i := 0; i < varsNum; i++ { + challengeNames[i+numChallenges-varsNum] = prefix + strconv.Itoa(i) + } + if settings.Transcript == nil { + transcript := fiatshamir.NewTranscript(settings.Hash, challengeNames...) + settings.Transcript = transcript + } + + for i := range settings.BaseChallenges { + if err = settings.Transcript.Bind(challengeNames[0], settings.BaseChallenges[i]); err != nil { + return + } + } + return +} + +func next(transcript *fiatshamir.Transcript, bindings []fr.Element, remainingChallengeNames *[]string) (fr.Element, error) { + challengeName := (*remainingChallengeNames)[0] + for i := range bindings { + bytes := bindings[i].Bytes() + if err := transcript.Bind(challengeName, bytes[:]); err != nil { + return fr.Element{}, err + } + } + var res fr.Element + bytes, err := transcript.ComputeChallenge(challengeName) + res.SetBytes(bytes) + + *remainingChallengeNames = (*remainingChallengeNames)[1:] + + return res, err +} + +// sumcheckProve create a non-interactive proof +func sumcheckProve(claims sumcheckClaims, transcriptSettings fiatshamir.Settings) (sumcheckProof, error) { + + var proof sumcheckProof + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return proof, err + } + + var combinationCoeff fr.Element + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return proof, err + } + } + + varsNum := claims.varsNum() + proof.partialSumPolys = make([]polynomial.Polynomial, varsNum) + proof.partialSumPolys[0] = claims.combine(combinationCoeff) + challenges := make([]fr.Element, varsNum) + + for j := 0; j+1 < varsNum; j++ { + if challenges[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return proof, err + } + proof.partialSumPolys[j+1] = claims.next(challenges[j]) + } + + if challenges[varsNum-1], err = next(transcript, proof.partialSumPolys[varsNum-1], &remainingChallengeNames); err != nil { + return proof, err + } + + proof.finalEvalProof = claims.proveFinalEval(challenges) + + return proof, nil +} + +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, transcriptSettings fiatshamir.Settings) error { + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return err + } + + var combinationCoeff fr.Element + + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []fr.Element{}, &remainingChallengeNames); err != nil { + return err + } + } + + r := make([]fr.Element, claims.varsNum()) + + // Just so that there is enough room for gJ to be reused + maxDegree := claims.degree(0) + for j := 1; j < claims.varsNum(); j++ { + if d := claims.degree(j); d > maxDegree { + maxDegree = d + } + } + gJ := make(polynomial.Polynomial, maxDegree+1) //At the end of iteration j, gJ = ∑_{i < 2ⁿ⁻ʲ⁻¹} g(X₁, ..., Xⱼ₊₁, i...) NOTE: n is shorthand for claims.varsNum() + gJR := claims.combinedSum(combinationCoeff) // At the beginning of iteration j, gJR = ∑_{i < 2ⁿ⁻ʲ} g(r₁, ..., rⱼ, i...) + + for j := range claims.varsNum() { + if len(proof.partialSumPolys[j]) != claims.degree(j) { + return errors.New("malformed proof") + } + copy(gJ[1:], proof.partialSumPolys[j]) + gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) // Requirement that gⱼ(0) + gⱼ(1) = gⱼ₋₁(r) + // gJ is ready + + //Prepare for the next iteration + if r[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return err + } + // This is an extremely inefficient way of interpolating. TODO: Interpolate without symbolically computing a polynomial + gJCoeffs := polynomial.InterpolateOnRange(gJ[:(claims.degree(j) + 1)]) + gJR = gJCoeffs.Eval(&r[j]) + } + + return claims.verifyFinalEval(r, combinationCoeff, gJR, proof.finalEvalProof) +} diff --git a/internal/gkr/bw6-761/sumcheck_test.go b/internal/gkr/bw6-761/sumcheck_test.go new file mode 100644 index 00000000..e7fbb14f --- /dev/null +++ b/internal/gkr/bw6-761/sumcheck_test.go @@ -0,0 +1,152 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/stretchr/testify/assert" + + "math/bits" + + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + + "strings" + "testing" +) + +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { + poly := make(polynomial.MultiLin, len(polyInt)) + for i, n := range polyInt { + poly[i].SetUint64(n) + } + + claim := singleMultilinClaim{g: poly.Clone()} + + proof, err := sumcheckProve(&claim, fiatshamir.WithHash(hashGenerator())) + if err != nil { + return err + } + + var sb strings.Builder + for _, p := range proof.partialSumPolys { + + sb.WriteString("\t{") + for i := 0; i < len(p); i++ { + sb.WriteString(p[i].String()) + if i+1 < len(p) { + sb.WriteString(", ") + } + } + sb.WriteString("}\n") + } + + lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if err = sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())); err != nil { + return err + } + + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())) == nil { + return fmt.Errorf("bad proof accepted") + } + return nil +} + +func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { + + polys := [][]uint64{ + {1, 2, 3, 4}, // 1 + 2X₁ + X₂ + {1, 2, 3, 4, 5, 6, 7, 8}, // 1 + 4X₁ + 2X₂ + X₃ + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, // 1 + 8X₁ + 4X₂ + 2X₃ + X₄ + } + + const MaxStep = 4 + const MaxStart = 4 + hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + + for step := 0; step < MaxStep; step++ { + for startState := 0; startState < MaxStart; startState++ { + if step == 0 && startState == 1 { // unlucky case where a bad proof would be accepted + continue + } + hashGens = append(hashGens, newMessageCounterGenerator(startState, step)) + } + } + + for _, poly := range polys { + for _, hashGen := range hashGens { + assert.NoError(t, testSumcheckSingleClaimMultilin(poly, hashGen), + "failed with poly %v and hashGen %v", poly, hashGen()) + } + } +} + +type singleMultilinClaim struct { + g polynomial.MultiLin +} + +func (c singleMultilinClaim) proveFinalEval(r []fr.Element) []fr.Element { + return nil // verifier can compute the final eval itself +} + +func (c singleMultilinClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} + +func (c singleMultilinClaim) claimsNum() int { + return 1 +} + +func sumForX1One(g polynomial.MultiLin) polynomial.Polynomial { + sum := g[len(g)/2] + for i := len(g)/2 + 1; i < len(g); i++ { + sum.Add(&sum, &g[i]) + } + return []fr.Element{sum} +} + +func (c singleMultilinClaim) combine(fr.Element) polynomial.Polynomial { + return sumForX1One(c.g) +} + +func (c *singleMultilinClaim) next(r fr.Element) polynomial.Polynomial { + c.g.Fold(r) + return sumForX1One(c.g) +} + +type singleMultilinLazyClaim struct { + g polynomial.MultiLin + claimedSum fr.Element +} + +func (c singleMultilinLazyClaim) verifyFinalEval(r []fr.Element, combinationCoeff fr.Element, purportedValue fr.Element, proof []fr.Element) error { + val := c.g.Evaluate(r, nil) + if val.Equal(&purportedValue) { + return nil + } + return fmt.Errorf("mismatch") +} + +func (c singleMultilinLazyClaim) combinedSum(combinationCoeffs fr.Element) fr.Element { + return c.claimedSum +} + +func (c singleMultilinLazyClaim) degree(i int) int { + return 1 +} + +func (c singleMultilinLazyClaim) claimsNum() int { + return 1 +} + +func (c singleMultilinLazyClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} diff --git a/internal/gkr/bw6-761/test_vector_utils_test.go b/internal/gkr/bw6-761/test_vector_utils_test.go new file mode 100644 index 00000000..1e556985 --- /dev/null +++ b/internal/gkr/bw6-761/test_vector_utils_test.go @@ -0,0 +1,144 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + "strings" + + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" + "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/polynomial" + "github.com/consensys/gnark/internal/gkr/gkrtesting" +) + +func toElement(i int64) *fr.Element { + var res fr.Element + res.SetInt64(i) + return &res +} + +func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { + if _type, ok := d["type"]; ok { + switch _type { + case "const": + startState := int64(d["val"].(float64)) + return &messageCounter{startState: startState, step: 0, state: startState}, nil + default: + return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) + } + } + return nil, fmt.Errorf("hash description missing type") +} + +type messageCounter struct { + startState int64 + state int64 + step int64 +} + +func (m *messageCounter) Write(p []byte) (n int, err error) { + inputBlockSize := (len(p)-1)/fr.Bytes + 1 + m.state += int64(inputBlockSize) * m.step + return len(p), nil +} + +func (m *messageCounter) Sum(b []byte) []byte { + inputBlockSize := (len(b)-1)/fr.Bytes + 1 + resI := m.state + int64(inputBlockSize)*m.step + var res fr.Element + res.SetInt64(int64(resI)) + resBytes := res.Bytes() + return resBytes[:] +} + +func (m *messageCounter) Reset() { + m.state = m.startState +} + +func (m *messageCounter) Size() int { + return fr.Bytes +} + +func (m *messageCounter) BlockSize() int { + return fr.Bytes +} + +func newMessageCounter(startState, step int) hash.Hash { + transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} + return transcript +} + +func newMessageCounterGenerator(startState, step int) func() hash.Hash { + return func() hash.Hash { + return newMessageCounter(startState, step) + } +} + +func setElement(z *fr.Element, value interface{}) (*fr.Element, error) { + + // TODO: Put this in element.SetString? + switch v := value.(type) { + case string: + + if sep := strings.Split(v, "/"); len(sep) == 2 { + var denom fr.Element + if _, err := z.SetString(sep[0]); err != nil { + return nil, err + } + if _, err := denom.SetString(sep[1]); err != nil { + return nil, err + } + denom.Inverse(&denom) + z.Mul(z, &denom) + return z, nil + } + + case float64: + asInt := int64(v) + if float64(asInt) != v { + return nil, fmt.Errorf("cannot currently parse float") + } + z.SetInt64(asInt) + return z, nil + } + + return z.SetInterface(value) +} + +func sliceToElementSlice[T any](slice []T) ([]fr.Element, error) { + elementSlice := make([]fr.Element, len(slice)) + for i, v := range slice { + if _, err := setElement(&elementSlice[i], v); err != nil { + return nil, err + } + } + return elementSlice, nil +} + +func sliceEquals(a []fr.Element, b []fr.Element) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if !a[i].Equal(&b[i]) { + return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) + } + } + return nil +} + +func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if err := sliceEquals(a[i], b[i]); err != nil { + return fmt.Errorf("at index %d: %w", i, err) + } + } + return nil +} diff --git a/internal/gkr/gkr.go b/internal/gkr/gkr.go new file mode 100644 index 00000000..955ad8a3 --- /dev/null +++ b/internal/gkr/gkr.go @@ -0,0 +1,385 @@ +package gkr + +import ( + "errors" + "fmt" + "strconv" + + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + fiatshamir "github.com/consensys/gnark/std/fiat-shamir" + "github.com/consensys/gnark/std/polynomial" +) + +// @tabaie TODO: Contains many things copy-pasted from gnark-crypto. Generify somehow? + +// A SNARK gadget capable of verifying a GKR proof +// The goal is to prove/verify evaluations of many instances of the same circuit. + +type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) + +// eqTimesGateEvalSumcheckLazyClaims is a lazy claim for sumcheck (verifier side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the checking of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckLazyClaims struct { + wireI int + evaluationPoints [][]frontend.Variable + claimedEvaluations []frontend.Variable + manager *claimsManager // WARNING: Circular references +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) getWire() *gkrtypes.Wire { + return e.manager.wires[e.wireI] +} + +// verifyFinalEval finalizes the verification of w. +// The prover's claims w(xᵢ) = yᵢ have already been reduced to verifying +// ∑ cⁱ eq(xᵢ, r) w(r) = purportedValue. ( c is combinationCoeff ) +// Both purportedValue and the vector r have been randomized during the sumcheck protocol. +// By taking the w term out of the sum we get the equivalent claim that +// for E := ∑ eq(xᵢ, r), it must be that E w(r) = purportedValue. +// If w is an input wire, the verifier can directly check its evaluation at r. +// Otherwise, the prover makes claims about the evaluation of w's input wires, +// wᵢ, at r, to be verified later. +// The claims are communicated through the proof parameter. +// The verifier checks here if the claimed evaluations of wᵢ(r) are consistent with +// the main claim, by checking E w(wᵢ(r)...) = purportedValue. +func (e *eqTimesGateEvalSumcheckLazyClaims) verifyFinalEval(api frontend.API, r []frontend.Variable, combinationCoeff, purportedValue frontend.Variable, uniqueInputEvaluations []frontend.Variable) error { + // the eq terms ( E ) + numClaims := len(e.evaluationPoints) + evaluation := polynomial.EvalEq(api, e.evaluationPoints[numClaims-1], r) + for i := numClaims - 2; i >= 0; i-- { + evaluation = api.Mul(evaluation, combinationCoeff) + eq := polynomial.EvalEq(api, e.evaluationPoints[i], r) + evaluation = api.Add(evaluation, eq) + } + + wire := e.getWire() + + // the g(...) term + var gateEvaluation frontend.Variable + if wire.IsInput() { + gateEvaluation = e.manager.assignment[e.wireI].Evaluate(api, r) + } else { + + injection, injectionLeftInv := + e.manager.wires.ClaimPropagationInfo(e.wireI) + + if len(injection) != len(uniqueInputEvaluations) { + return fmt.Errorf("%d input wire evaluations given, %d expected", len(uniqueInputEvaluations), len(injection)) + } + + for uniqueI, i := range injection { // map from unique to all + e.manager.add(wire.Inputs[i], r, uniqueInputEvaluations[uniqueI]) + } + + inputEvaluations := make([]frontend.Variable, len(wire.Inputs)) + for i, uniqueI := range injectionLeftInv { // map from all to unique + inputEvaluations[i] = uniqueInputEvaluations[uniqueI] + } + + gateEvaluation = wire.Gate.Evaluate(api, inputEvaluations...) + } + evaluation = api.Mul(evaluation, gateEvaluation) + + api.AssertIsEqual(evaluation, purportedValue) + return nil +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) claimsNum() int { + return len(e.evaluationPoints) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) varsNum() int { + return len(e.evaluationPoints[0]) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) combinedSum(api frontend.API, a frontend.Variable) frontend.Variable { + evalsAsPoly := polynomial.Polynomial(e.claimedEvaluations) + return evalsAsPoly.Eval(api, a) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) degree(int) int { + return 1 + e.getWire().Gate.Degree() +} + +type claimsManager struct { + claims []*eqTimesGateEvalSumcheckLazyClaims + assignment gkrtypes.WireAssignment + wires gkrtypes.Wires +} + +func newClaimsManager(wires gkrtypes.Wires, assignment gkrtypes.WireAssignment) (claims claimsManager) { + claims.assignment = assignment + claims.claims = make([]*eqTimesGateEvalSumcheckLazyClaims, len(wires)) + claims.wires = wires + + for i := range wires { + wire := wires[i] + claims.claims[i] = &eqTimesGateEvalSumcheckLazyClaims{ + wireI: i, + evaluationPoints: make([][]frontend.Variable, 0, wire.NbClaims()), + claimedEvaluations: make(polynomial.Polynomial, wire.NbClaims()), + manager: &claims, + } + } + return +} + +func (m *claimsManager) add(wire int, evaluationPoint []frontend.Variable, evaluation frontend.Variable) { + claim := m.claims[wire] + i := len(claim.evaluationPoints) + claim.claimedEvaluations[i] = evaluation + claim.evaluationPoints = append(claim.evaluationPoints, evaluationPoint) +} + +func (m *claimsManager) getLazyClaim(wire int) *eqTimesGateEvalSumcheckLazyClaims { + return m.claims[wire] +} + +func (m *claimsManager) deleteClaim(wire int) { + m.claims[wire].manager = nil + m.claims[wire] = nil +} + +type settings struct { + sorted []*gkrtypes.Wire + transcript *fiatshamir.Transcript + transcriptPrefix string + nbVars int +} + +type Option func(*settings) + +func WithSortedCircuit(sorted []*gkrtypes.Wire) Option { + return func(options *settings) { + options.sorted = sorted + } +} + +func setup(api frontend.API, c gkrtypes.Circuit, assignment gkrtypes.WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (settings, error) { + var o settings + var err error + for _, option := range options { + option(&o) + } + + o.nbVars = assignment.NbVars() + nbInstances := assignment.NbInstances() + if 1< 1 { //combine the claims + size++ + } + size += logNbInstances // full run of sumcheck on logNbInstances variables + } + + nums := make([]string, max(len(sorted), logNbInstances)) + for i := range nums { + nums[i] = strconv.Itoa(i) + } + + challenges := make([]string, size) + + // output wire claims + firstChallengePrefix := prefix + "fC." + for j := 0; j < logNbInstances; j++ { + challenges[j] = firstChallengePrefix + nums[j] + } + j := logNbInstances + for i := len(sorted) - 1; i >= 0; i-- { + if sorted[i].NoProof() { + continue + } + wirePrefix := prefix + "w" + nums[i] + "." + + if sorted[i].NbClaims() > 1 { + challenges[j] = wirePrefix + "comb" + j++ + } + + partialSumPrefix := wirePrefix + "pSP." + for k := 0; k < logNbInstances; k++ { + challenges[j] = partialSumPrefix + nums[k] + j++ + } + } + return challenges +} + +func getFirstChallengeNames(logNbInstances int, prefix string) []string { + res := make([]string, logNbInstances) + firstChallengePrefix := prefix + "fC." + for i := 0; i < logNbInstances; i++ { + res[i] = firstChallengePrefix + strconv.Itoa(i) + } + return res +} + +func getChallenges(transcript *fiatshamir.Transcript, names []string) (challenges []frontend.Variable, err error) { + challenges = make([]frontend.Variable, len(names)) + for i, name := range names { + if challenges[i], err = transcript.ComputeChallenge(name); err != nil { + return + } + } + return +} + +// Verify the consistency of the claimed output with the claimed input +// Unlike in Prove, the assignment argument need not be complete +func Verify(api frontend.API, c gkrtypes.Circuit, assignment gkrtypes.WireAssignment, proof Proof, transcriptSettings fiatshamir.Settings, options ...Option) error { + o, err := setup(api, c, assignment, transcriptSettings, options...) + if err != nil { + return err + } + + claims := newClaimsManager(o.sorted, assignment) + + var firstChallenge []frontend.Variable + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge []frontend.Variable + for i := len(c) - 1; i >= 0; i-- { + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(api, firstChallenge)) + } + + proofW := proof[i] + claim := claims.getLazyClaim(i) + if wire.NoProof() { // input wires with one claim only + // make sure the proof is empty + if len(proofW.FinalEvalProof) != 0 || len(proofW.PartialSumPolys) != 0 { + return errors.New("no proof allowed for input wire with a single claim") + } + + if wire.NbClaims() == 1 { // input wire + // simply evaluate and see if it matches + evaluation := assignment[i].Evaluate(api, claim.evaluationPoints[0]) + api.AssertIsEqual(claim.claimedEvaluations[0], evaluation) + } + } else if err = verifySumcheck( + api, claim, proof[i], fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err == nil { + baseChallenge = proofW.FinalEvalProof + } else { + return err + } + claims.deleteClaim(i) + } + return nil +} + +// TODO: Have this use algo_utils.TopologicalSort underneath + +func (p Proof) Serialize() []frontend.Variable { + size := 0 + for i := range p { + for j := range p[i].PartialSumPolys { + size += len(p[i].PartialSumPolys[j]) + } + size += len(p[i].FinalEvalProof) + } + + res := make([]frontend.Variable, 0, size) + for i := range p { + for j := range p[i].PartialSumPolys { + res = append(res, p[i].PartialSumPolys[j]...) + } + res = append(res, p[i].FinalEvalProof...) + } + if len(res) != size { + panic("bug") // TODO: Remove + } + return res +} + +func computeLogNbInstances(wires []*gkrtypes.Wire, serializedProofLen int) int { + partialEvalElemsPerVar := 0 + for _, w := range wires { + if !w.NoProof() { + partialEvalElemsPerVar += w.Gate.Degree() + 1 + } + serializedProofLen -= w.NbUniqueOutputs + } + return serializedProofLen / partialEvalElemsPerVar +} + +type variablesReader []frontend.Variable + +func (r *variablesReader) nextN(n int) []frontend.Variable { + res := (*r)[:n] + *r = (*r)[n:] + return res +} + +func (r *variablesReader) hasNextN(n int) bool { + return len(*r) >= n +} + +func DeserializeProof(sorted []*gkrtypes.Wire, serializedProof []frontend.Variable) (Proof, error) { + proof := make(Proof, len(sorted)) + logNbInstances := computeLogNbInstances(sorted, len(serializedProof)) + + reader := variablesReader(serializedProof) + for i, wI := range sorted { + if !wI.NoProof() { + proof[i].PartialSumPolys = make([]polynomial.Polynomial, logNbInstances) + for j := range proof[i].PartialSumPolys { + proof[i].PartialSumPolys[j] = reader.nextN(wI.Gate.Degree() + 1) + } + } + proof[i].FinalEvalProof = reader.nextN(wI.NbUniqueInputs()) + } + if reader.hasNextN(1) { + return nil, fmt.Errorf("proof too long: expected %d encountered %d", len(serializedProof)-len(reader), len(serializedProof)) + } + return proof, nil +} diff --git a/std/gkr/gkr_test.go b/internal/gkr/gkr_test.go similarity index 60% rename from std/gkr/gkr_test.go rename to internal/gkr/gkr_test.go index d24b25a9..faf8eadc 100644 --- a/std/gkr/gkr_test.go +++ b/internal/gkr/gkr_test.go @@ -10,12 +10,13 @@ import ( "github.com/consensys/gnark/backend" "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/consensys/gnark/internal/gkr/gkrtypes" fiatshamir "github.com/consensys/gnark/std/fiat-shamir" + "github.com/consensys/gnark/std/hash" "github.com/consensys/gnark/std/polynomial" "github.com/consensys/gnark/test" "github.com/stretchr/testify/assert" - - "github.com/consensys/gnark/std/hash" ) func TestGkrVectors(t *testing.T) { @@ -113,7 +114,7 @@ func (c *GkrVerifierCircuit) Define(api frontend.API) error { if testCase, err = getTestCase(c.TestCaseName); err != nil { return err } - sorted := topologicalSort(testCase.Circuit) + sorted := testCase.Circuit.TopologicalSort() if proof, err = DeserializeProof(sorted, c.SerializedProof); err != nil { return err @@ -132,16 +133,16 @@ func (c *GkrVerifierCircuit) Define(api frontend.API) error { return Verify(api, testCase.Circuit, assignment, proof, fiatshamir.WithHash(hsh)) } -func makeInOutAssignment(c Circuit, inputValues [][]frontend.Variable, outputValues [][]frontend.Variable) WireAssignment { - sorted := topologicalSort(c) - res := make(WireAssignment, len(inputValues)+len(outputValues)) +func makeInOutAssignment(c gkrtypes.Circuit, inputValues [][]frontend.Variable, outputValues [][]frontend.Variable) gkrtypes.WireAssignment { + sorted := c.TopologicalSort() + res := make(gkrtypes.WireAssignment, len(c)) inI, outI := 0, 0 - for _, w := range sorted { + for wI, w := range sorted { if w.IsInput() { - res[w] = inputValues[inI] + res[wI] = inputValues[inI] inI++ } else if w.IsOutput() { - res[w] = outputValues[outI] + res[wI] = outputValues[outI] outI++ } } @@ -155,7 +156,7 @@ func fillWithBlanks(slice [][]frontend.Variable, size int) { } type TestCase struct { - Circuit Circuit + Circuit gkrtypes.Circuit Hash HashDescription Proof Proof Input [][]frontend.Variable @@ -190,14 +191,12 @@ func getTestCase(path string) (*TestCase, error) { return nil, err } - if cse.Circuit, err = getCircuit(filepath.Join(dir, info.Circuit)); err != nil { - return nil, err - } + cse.Circuit = cache.GetCircuit(filepath.Join(dir, info.Circuit)) cse.Proof = unmarshalProof(info.Proof) - cse.Input = ToVariableSliceSlice(info.Input) - cse.Output = ToVariableSliceSlice(info.Output) + cse.Input = toVariableSliceSlice(info.Input) + cse.Output = toVariableSliceSlice(info.Output) cse.Hash = info.Hash cse.Name = path testCases[path] = cse @@ -209,69 +208,6 @@ func getTestCase(path string) (*TestCase, error) { return cse, nil } -type WireInfo struct { - Gate string `json:"gate"` - Inputs []int `json:"inputs"` -} - -type CircuitInfo []WireInfo - -var circuitCache = make(map[string]Circuit) - -func getCircuit(path string) (circuit Circuit, err error) { - path, err = filepath.Abs(path) - if err != nil { - return - } - var ok bool - if circuit, ok = circuitCache[path]; ok { - return - } - var bytes []byte - if bytes, err = os.ReadFile(path); err == nil { - var circuitInfo CircuitInfo - if err = json.Unmarshal(bytes, &circuitInfo); err == nil { - circuit, err = circuitInfo.toCircuit() - if err == nil { - circuitCache[path] = circuit - } - } - } - return -} - -func (c CircuitInfo) toCircuit() (circuit Circuit, err error) { - circuit = make(Circuit, len(c)) - for i, wireInfo := range c { - circuit[i].Inputs = make([]*Wire, len(wireInfo.Inputs)) - for iAsInput, iAsWire := range wireInfo.Inputs { - input := &circuit[iAsWire] - circuit[i].Inputs[iAsInput] = input - } - - var found bool - if circuit[i].Gate, found = Gates[wireInfo.Gate]; !found && wireInfo.Gate != "" { - err = fmt.Errorf("undefined gate \"%s\"", wireInfo.Gate) - } - } - - return -} - -type _select int - -func init() { - Gates["select-input-3"] = _select(2) -} - -func (g _select) Evaluate(_ frontend.API, in ...frontend.Variable) frontend.Variable { - return in[g] -} - -func (g _select) Degree() int { - return 1 -} - type PrintableProof []PrintableSumcheckProof type PrintableSumcheckProof struct { @@ -287,7 +223,7 @@ func unmarshalProof(printable PrintableProof) (proof Proof) { finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) finalEvalProof := make([]frontend.Variable, finalEvalSlice.Len()) for k := range finalEvalProof { - finalEvalProof[k] = ToVariable(finalEvalSlice.Index(k).Interface()) + finalEvalProof[k] = toVariable(finalEvalSlice.Index(k).Interface()) } proof[i].FinalEvalProof = finalEvalProof } else { @@ -296,7 +232,7 @@ func unmarshalProof(printable PrintableProof) (proof Proof) { proof[i].PartialSumPolys = make([]polynomial.Polynomial, len(printable[i].PartialSumPolys)) for k := range printable[i].PartialSumPolys { - proof[i].PartialSumPolys[k] = ToVariableSlice(printable[i].PartialSumPolys[k]) + proof[i].PartialSumPolys[k] = toVariableSlice(printable[i].PartialSumPolys[k]) } } return @@ -307,7 +243,7 @@ func TestLogNbInstances(t *testing.T) { return func(t *testing.T) { testCase, err := getTestCase(path) assert.NoError(t, err) - wires := topologicalSort(testCase.Circuit) + wires := testCase.Circuit.TopologicalSort() serializedProof := testCase.Proof.Serialize() logNbInstances := computeLogNbInstances(wires, len(serializedProof)) assert.Equal(t, 1, logNbInstances) @@ -321,107 +257,6 @@ func TestLogNbInstances(t *testing.T) { } } -func TestLoadCircuit(t *testing.T) { - c, err := getCircuit("test_vectors/resources/two_identity_gates_composed_single_input.json") - assert.NoError(t, err) - assert.Equal(t, []*Wire{}, c[0].Inputs) - assert.Equal(t, []*Wire{&c[0]}, c[1].Inputs) - assert.Equal(t, []*Wire{&c[1]}, c[2].Inputs) - -} - -func TestTopSortTrivial(t *testing.T) { - c := make(Circuit, 2) - c[0].Inputs = []*Wire{&c[1]} - sorted := topologicalSort(c) - assert.Equal(t, []*Wire{&c[1], &c[0]}, sorted) -} - -func TestTopSortSingleGate(t *testing.T) { - c := make(Circuit, 3) - c[0].Inputs = []*Wire{&c[1], &c[2]} - sorted := topologicalSort(c) - expected := []*Wire{&c[1], &c[2], &c[0]} - assert.True(t, SliceEqual(sorted, expected)) //TODO: Remove - AssertSliceEqual(t, sorted, expected) - assert.Equal(t, c[0].nbUniqueOutputs, 0) - assert.Equal(t, c[1].nbUniqueOutputs, 1) - assert.Equal(t, c[2].nbUniqueOutputs, 1) -} - -func TestTopSortDeep(t *testing.T) { - c := make(Circuit, 4) - c[0].Inputs = []*Wire{&c[2]} - c[1].Inputs = []*Wire{&c[3]} - c[2].Inputs = []*Wire{} - c[3].Inputs = []*Wire{&c[0]} - sorted := topologicalSort(c) - assert.Equal(t, []*Wire{&c[2], &c[0], &c[3], &c[1]}, sorted) -} - -func TestTopSortWide(t *testing.T) { - c := make(Circuit, 10) - c[0].Inputs = []*Wire{&c[3], &c[8]} - c[1].Inputs = []*Wire{&c[6]} - c[2].Inputs = []*Wire{&c[4]} - c[3].Inputs = []*Wire{} - c[4].Inputs = []*Wire{} - c[5].Inputs = []*Wire{&c[9]} - c[6].Inputs = []*Wire{&c[9]} - c[7].Inputs = []*Wire{&c[9], &c[5], &c[2]} - c[8].Inputs = []*Wire{&c[4], &c[3]} - c[9].Inputs = []*Wire{} - - sorted := topologicalSort(c) - sortedExpected := []*Wire{&c[3], &c[4], &c[2], &c[8], &c[0], &c[9], &c[5], &c[6], &c[1], &c[7]} - - assert.Equal(t, sortedExpected, sorted) -} - -func ToVariable(v interface{}) frontend.Variable { - switch vT := v.(type) { - case float64: - return int(vT) - default: - return v - } -} - -func ToVariableSlice[V any](slice []V) (variableSlice []frontend.Variable) { - variableSlice = make([]frontend.Variable, len(slice)) - for i := range slice { - variableSlice[i] = ToVariable(slice[i]) - } - return -} - -func ToVariableSliceSlice[V any](sliceSlice [][]V) (variableSliceSlice [][]frontend.Variable) { - variableSliceSlice = make([][]frontend.Variable, len(sliceSlice)) - for i := range sliceSlice { - variableSliceSlice[i] = ToVariableSlice(sliceSlice[i]) - } - return -} - -func AssertSliceEqual[T comparable](t *testing.T, expected, seen []T) { - assert.Equal(t, len(expected), len(seen)) - for i := range seen { - assert.True(t, expected[i] == seen[i], "@%d: %v != %v", i, expected[i], seen[i]) // assert.Equal is not strict enough when comparing pointers, i.e. it compares what they refer to - } -} - -func SliceEqual[T comparable](expected, seen []T) bool { - if len(expected) != len(seen) { - return false - } - for i := range seen { - if expected[i] != seen[i] { - return false - } - } - return true -} - type HashDescription map[string]interface{} func HashFromDescription(api frontend.API, d HashDescription) (hash.FieldHasher, error) { @@ -499,24 +334,8 @@ func TestConstHash(t *testing.T) { ) } -var mimcSnarkTotalCalls = 0 - -type MiMCCipherGate struct { - Ark frontend.Variable -} +var cache *gkrtesting.Cache -func (m MiMCCipherGate) Evaluate(api frontend.API, input ...frontend.Variable) frontend.Variable { - mimcSnarkTotalCalls++ - - if len(input) != 2 { - panic("mimc has fan-in 2") - } - sum := api.Add(input[0], input[1], m.Ark) - - sumCubed := api.Mul(sum, sum, sum) // sum^3 - return api.Mul(sumCubed, sumCubed, sum) -} - -func (m MiMCCipherGate) Degree() int { - return 7 +func init() { + cache = gkrtesting.NewCache() } diff --git a/internal/gkr/gkrinfo/info.go b/internal/gkr/gkrinfo/info.go new file mode 100644 index 00000000..de9a845e --- /dev/null +++ b/internal/gkr/gkrinfo/info.go @@ -0,0 +1,143 @@ +// Package gkrinfo contains serializable information capable of being saved in a SNARK circuit CS object. +package gkrinfo + +import ( + "fmt" + "sort" + + "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/internal/utils" +) + +type ( + InputDependency struct { + OutputWire int + OutputInstance int + InputInstance int + } + + Wire struct { + Gate string + Inputs []int + NbUniqueOutputs int + } + + Circuit []Wire + + PrintInfo struct { + Values []any + Instance uint32 + IsGkrVar []bool + } + StoringInfo struct { + Circuit Circuit + Dependencies [][]InputDependency // nil for input wires + NbInstances int + HashName string + SolveHintID solver.HintID + ProveHintID solver.HintID + Prints []PrintInfo + } + + Permutations struct { + SortedInstances []int + SortedWires []int + InstancesPermutation []int + WiresPermutation []int + } +) + +func (w Wire) IsInput() bool { + return len(w.Inputs) == 0 +} + +func (w Wire) IsOutput() bool { + return w.NbUniqueOutputs == 0 +} + +func (d *StoringInfo) NewInputVariable() int { + i := len(d.Circuit) + d.Circuit = append(d.Circuit, Wire{}) + d.Dependencies = append(d.Dependencies, nil) + return i +} + +// Compile sorts the Circuit wires, their dependencies and the instances +func (d *StoringInfo) Compile(nbInstances int) (Permutations, error) { + + var p Permutations + d.NbInstances = nbInstances + // sort the instances to decide the order in which they are to be solved + instanceDeps := make([][]int, nbInstances) + for i := range d.Circuit { + for _, dep := range d.Dependencies[i] { + instanceDeps[dep.InputInstance] = append(instanceDeps[dep.InputInstance], dep.OutputInstance) + } + } + + p.SortedInstances, _ = utils.TopologicalSort(instanceDeps) + p.InstancesPermutation = utils.InvertPermutation(p.SortedInstances) + + // this whole circuit sorting is a bit of a charade. if things are built using an api, there's no way it could NOT already be topologically sorted + // worth keeping for future-proofing? + + inputs := utils.Map(d.Circuit, func(w Wire) []int { + return w.Inputs + }) + + var uniqueOuts [][]int + p.SortedWires, uniqueOuts = utils.TopologicalSort(inputs) + p.WiresPermutation = utils.InvertPermutation(p.SortedWires) + wirePermutationAt := utils.SliceAt(p.WiresPermutation) + sorted := make([]Wire, len(d.Circuit)) // TODO: Directly manipulate d.circuit instead + sortedDeps := make([][]InputDependency, len(d.Circuit)) + + // go through the wires in the sorted order and fix the input and dependency indices according to the permutations + for newI, oldI := range p.SortedWires { + oldW := d.Circuit[oldI] + + for depI := range d.Dependencies[oldI] { + dep := &d.Dependencies[oldI][depI] + dep.OutputWire = p.WiresPermutation[dep.OutputWire] + dep.InputInstance = p.InstancesPermutation[dep.InputInstance] + dep.OutputInstance = p.InstancesPermutation[dep.OutputInstance] + } + sort.Slice(d.Dependencies[oldI], func(i, j int) bool { + return d.Dependencies[oldI][i].InputInstance < d.Dependencies[oldI][j].InputInstance + }) + for i := 1; i < len(d.Dependencies[oldI]); i++ { + if d.Dependencies[oldI][i].InputInstance == d.Dependencies[oldI][i-1].InputInstance { + return p, fmt.Errorf("an input wire can only have one dependency per instance") + } + } // TODO: Check that dependencies and explicit assignments cover all instances + + sortedDeps[newI] = d.Dependencies[oldI] + sorted[newI] = Wire{ + Gate: oldW.Gate, + Inputs: utils.Map(oldW.Inputs, wirePermutationAt), + NbUniqueOutputs: len(uniqueOuts[oldI]), + } + } + + // re-arrange the prints + for i := range d.Prints { + for j, isVar := range d.Prints[i].IsGkrVar { + if isVar { + d.Prints[i].Values[j] = uint32(p.WiresPermutation[d.Prints[i].Values[j].(uint32)]) + } + } + } + + d.Circuit, d.Dependencies = sorted, sortedDeps + + return p, nil +} + +func (d *StoringInfo) Is() bool { + return d.Circuit != nil +} + +// A ConstraintSystem that supports GKR +type ConstraintSystem interface { + SetGkrInfo(info StoringInfo) error +} diff --git a/internal/gkr/gkrtesting/gkrtesting.go b/internal/gkr/gkrtesting/gkrtesting.go new file mode 100644 index 00000000..ce9ba889 --- /dev/null +++ b/internal/gkr/gkrtesting/gkrtesting.go @@ -0,0 +1,115 @@ +package gkrtesting + +import ( + "encoding/json" + "errors" + "os" + "path/filepath" + + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrinfo" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// Cache for circuits and gates. +// The main functionality is to cache whole circuits, but this package needs to use its own gate registry, in order to avoid import cycles. +// Cache is used in tests for the per-curve GKR packages, but they in turn provide gate degree discovery functions to the gkrgates package. +type Cache struct { + circuits map[string]gkrtypes.Circuit + gates map[gkr.GateName]*gkrtypes.Gate +} + +func NewCache() *Cache { + gates := make(map[gkr.GateName]*gkrtypes.Gate, 7) + gates[gkr.Identity] = gkrtypes.Identity() + gates[gkr.Add2] = gkrtypes.Add2() + gates[gkr.Sub2] = gkrtypes.Sub2() + gates[gkr.Neg] = gkrtypes.Neg() + gates[gkr.Mul2] = gkrtypes.Mul2() + gates["mimc"] = gkrtypes.NewGate(func(api gkr.GateAPI, input ...frontend.Variable) frontend.Variable { + sum := api.Add(input[0], input[1]) //.Add(&sum, &m.ark) TODO: add ark + res := api.Mul(sum, sum) // sum^2 + res = api.Mul(res, sum) // sum^3 + res = api.Mul(res, res) // sum^6 + res = api.Mul(res, sum) // sum^7 + + return res + }, 2, 7, -1) + gates["select-input-3"] = gkrtypes.NewGate(func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return in[2] + }, 3, 1, 0) + + return &Cache{ + circuits: make(map[string]gkrtypes.Circuit), + gates: gates, + } +} + +func (c *Cache) GetCircuit(path string) (circuit gkrtypes.Circuit) { + path, err := filepath.Abs(path) + if err != nil { + panic(err) + } + var ok bool + if circuit, ok = c.circuits[path]; ok { + return + } + + var bytes []byte + if bytes, err = os.ReadFile(path); err != nil { + panic(err) + } + var circuitInfo gkrinfo.Circuit + if err = json.Unmarshal(bytes, &circuitInfo); err != nil { + panic(err) + } + if circuit, err = gkrtypes.CircuitInfoToCircuit(circuitInfo, c.GetGate); err != nil { + panic(err) + } + c.circuits[path] = circuit + + return +} + +func (c *Cache) RegisterGate(name gkr.GateName, gate *gkrtypes.Gate) { + if _, ok := c.gates[name]; ok { + panic("gate already registered") + } + c.gates[name] = gate +} + +func (c *Cache) GetGate(name gkr.GateName) *gkrtypes.Gate { + if gate, ok := c.gates[name]; ok { + return gate + } + panic("gate not found") +} + +type PrintableProof []PrintableSumcheckProof + +type PrintableSumcheckProof struct { + FinalEvalProof interface{} `json:"finalEvalProof"` + PartialSumPolys [][]interface{} `json:"partialSumPolys"` +} + +type HashDescription map[string]interface{} +type TestCaseInfo struct { + Hash HashDescription `json:"hash"` + Circuit string `json:"circuit"` + Input [][]interface{} `json:"input"` + Output [][]interface{} `json:"output"` + Proof PrintableProof `json:"proof"` +} + +func (c *Cache) ReadTestCaseInfo(filePath string) (info TestCaseInfo, err error) { + f, err := os.Open(filePath) + if err != nil { + return + } + defer func() { + err = errors.Join(err, f.Close()) + }() + err = json.NewDecoder(f).Decode(&info) + return +} diff --git a/internal/gkr/gkrtesting/gkrtesting_test.go b/internal/gkr/gkrtesting/gkrtesting_test.go new file mode 100644 index 00000000..8081343c --- /dev/null +++ b/internal/gkr/gkrtesting/gkrtesting_test.go @@ -0,0 +1,15 @@ +package gkrtesting + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestLoadCircuit(t *testing.T) { + cache := NewCache() + c := cache.GetCircuit("../test_vectors/circuits/two_identity_gates_composed_single_input.json") + assert.Equal(t, 0, len(c[0].Inputs)) + assert.Equal(t, []int{0}, c[1].Inputs) + assert.Equal(t, []int{1}, c[2].Inputs) +} diff --git a/internal/gkr/gkrtypes/topological_sort_test.go b/internal/gkr/gkrtypes/topological_sort_test.go new file mode 100644 index 00000000..19d5a2fa --- /dev/null +++ b/internal/gkr/gkrtypes/topological_sort_test.go @@ -0,0 +1,73 @@ +package gkrtypes + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTopSortTrivial(t *testing.T) { + c := make(Circuit, 2) + c[0].Inputs = []int{1} + sorted := c.TopologicalSort() + assert.Equal(t, []*Wire{&c[1], &c[0]}, sorted) +} + +func TestTopSortSingleGate(t *testing.T) { + c := make(Circuit, 3) + c[0].Inputs = []int{1, 2} + sorted := c.TopologicalSort() + expected := []*Wire{&c[1], &c[2], &c[0]} + + assert.Equal(t, expected, sorted) + assert.Equal(t, c[0].NbUniqueOutputs, 0) + assert.Equal(t, c[1].NbUniqueOutputs, 1) + assert.Equal(t, c[2].NbUniqueOutputs, 1) +} + +func TestTopSortDeep(t *testing.T) { + c := make(Circuit, 4) + c[0].Inputs = []int{2} + c[1].Inputs = []int{3} + c[2].Inputs = []int{} + c[3].Inputs = []int{0} + sorted := c.TopologicalSort() + assert.Equal(t, []*Wire{&c[2], &c[0], &c[3], &c[1]}, sorted) +} + +func TestTopSortWide(t *testing.T) { + c := make(Circuit, 10) + c[0].Inputs = []int{3, 8} + c[1].Inputs = []int{6} + c[2].Inputs = []int{4} + c[3].Inputs = []int{} + c[4].Inputs = []int{} + c[5].Inputs = []int{9} + c[6].Inputs = []int{9} + c[7].Inputs = []int{9, 5, 2} + c[8].Inputs = []int{4, 3} + c[9].Inputs = []int{} + + sorted := c.TopologicalSort() + sortedExpected := []*Wire{&c[3], &c[4], &c[2], &c[8], &c[0], &c[9], &c[5], &c[6], &c[1], &c[7]} + + assert.Equal(t, sortedExpected, sorted) +} + +func assertPermutation(t *testing.T, original Circuit, permuted []*Wire, permutationInv []int) { + for i := range permuted { + if permuted[i] != &original[permutationInv[i]] { + actualIndex := -1 + for j := range original { + if &original[j] == permuted[i] { + actualIndex = j + break + } + } + require.NotEqual(t, -1, actualIndex, "result is not a permutation. element #%d of \"permuted\" list not found in original", i) + t.Errorf("expected ") + t.Fail() + } + } +} diff --git a/internal/gkr/gkrtypes/types.go b/internal/gkr/gkrtypes/types.go new file mode 100644 index 00000000..7aed5ccd --- /dev/null +++ b/internal/gkr/gkrtypes/types.go @@ -0,0 +1,420 @@ +package gkrtypes + +import ( + "errors" + "fmt" + + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrinfo" + "github.com/consensys/gnark/internal/utils" + "github.com/consensys/gnark/std/gkrapi/gkr" + "github.com/consensys/gnark/std/polynomial" +) + +// A Gate is a low-degree multivariate polynomial +type Gate struct { + evaluate gkr.GateFunction // Evaluate the polynomial function defining the gate + nbIn int // number of inputs + degree int // total degree of the polynomial + solvableVar int // if there is a variable whose value can be uniquely determined from the value of the gate and the other inputs, its index, -1 otherwise +} + +func NewGate(f gkr.GateFunction, nbIn int, degree int, solvableVar int) *Gate { + return &Gate{ + evaluate: f, + nbIn: nbIn, + degree: degree, + solvableVar: solvableVar, + } +} + +func (g *Gate) Evaluate(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return g.evaluate(api, in...) +} + +// Degree returns the total degree of the gate's polynomial e.g. Degree(xy²) = 3 +func (g *Gate) Degree() int { + return g.degree +} + +// SolvableVar returns the index of a variable of degree 1 in the gate's polynomial. If there is no such variable, it returns -1. +func (g *Gate) SolvableVar() int { + return g.solvableVar +} + +// NbIn returns the number of inputs to the gate (its fan-in) +func (g *Gate) NbIn() int { + return g.nbIn +} + +type Wire struct { + Gate *Gate + Inputs []int + NbUniqueOutputs int +} + +func (w Wire) IsInput() bool { + return len(w.Inputs) == 0 +} + +func (w Wire) IsOutput() bool { + return w.NbUniqueOutputs == 0 +} + +func (w Wire) NbClaims() int { + if w.IsOutput() { + return 1 + } + return w.NbUniqueOutputs +} + +func (w Wire) NoProof() bool { + return w.IsInput() && w.NbClaims() == 1 +} + +func (w Wire) NbUniqueInputs() int { + set := make(map[int]struct{}, len(w.Inputs)) + for _, in := range w.Inputs { + set[in] = struct{}{} + } + return len(set) +} + +type ( + Circuit []Wire + Wires []*Wire +) + +// ClaimPropagationInfo returns sets of indices describing the pruning of claim propagation. +// At the end of sumcheck for wire #wireIndex, we end up with sequences "uniqueEvaluations" and "evaluations", +// the former a subsequence of the latter. +// injection are the indices of the unique evaluations in the original evaluation list. +// injectionRightInverse are the indices of the original evaluations in the unique evaluations list. +// There are no guarantees on the non-unique choice of the semi-inverse map. +func (wires Wires) ClaimPropagationInfo(wireIndex int) (injection, injectionLeftInverse []int) { + w := wires[wireIndex] + indexInProof := makeNeg1Slice(len(wires)) // O(n); use a map instead if it caused performance issues + injection = make([]int, 0, len(w.Inputs)) + injectionLeftInverse = make([]int, len(w.Inputs)) + + for inI, in := range w.Inputs { + if indexInProof[in] == -1 { // not found + indexInProof[in] = len(injection) + injection = append(injection, inI) + } + injectionLeftInverse[inI] = indexInProof[in] + } + + return +} + +func (c Circuit) maxGateDegree() int { + res := 1 + for i := range c { + if !c[i].IsInput() { + res = max(res, c[i].Gate.Degree()) + } + } + return res +} + +// MemoryRequirements returns an increasing vector of memory allocation sizes required for proving a GKR statement +func (c Circuit) MemoryRequirements(nbInstances int) []int { + res := []int{256, nbInstances, nbInstances * (c.maxGateDegree() + 1)} + + if res[0] > res[1] { // make sure it's sorted + res[0], res[1] = res[1], res[0] + if res[1] > res[2] { + res[1], res[2] = res[2], res[1] + } + } + + return res +} + +type SolvingInfo struct { + Circuit Circuit + Dependencies [][]gkrinfo.InputDependency + NbInstances int + HashName string + Prints []gkrinfo.PrintInfo +} + +// Chunks returns intervals of instances that are independent of each other and can be solved in parallel +func (info *SolvingInfo) Chunks() []int { + res := make([]int, 0, 1) + lastSeenDependencyI := make([]int, len(info.Circuit)) + + for start, end := 0, 0; start != info.NbInstances; start = end { + end = info.NbInstances + endWireI := -1 + for wI := range info.Circuit { + deps := info.Dependencies[wI] + if wDepI := lastSeenDependencyI[wI]; wDepI < len(deps) && deps[wDepI].InputInstance < end { + end = deps[wDepI].InputInstance + endWireI = wI + } + } + if endWireI != -1 { + lastSeenDependencyI[endWireI]++ + } + res = append(res, end) + } + return res +} + +// AssignmentOffsets describes the input layout of the Solve hint, by returning +// for each wire, the index of the first hint input element corresponding to it. +func (info *SolvingInfo) AssignmentOffsets() []int { + c := info.Circuit + res := make([]int, len(c)+1) + for i := range c { + nbExplicitAssignments := 0 + if c[i].IsInput() { + nbExplicitAssignments = info.NbInstances - len(info.Dependencies[i]) + } + res[i+1] = res[i] + nbExplicitAssignments + } + return res +} + +// OutputsList for each wire, returns the set of indexes of wires it is input to. +// It also sets the NbUniqueOutputs fields, and sets the wire metadata. +func (c Circuit) OutputsList() [][]int { + idGate := Identity() + res := make([][]int, len(c)) + for i := range c { + res[i] = make([]int, 0) + c[i].NbUniqueOutputs = 0 + if c[i].IsInput() { + c[i].Gate = idGate + } + } + ins := make(map[int]struct{}, len(c)) + for i := range c { + for k := range ins { // clear map + delete(ins, k) + } + for _, in := range c[i].Inputs { + res[in] = append(res[in], i) + if _, ok := ins[in]; !ok { + c[in].NbUniqueOutputs++ + ins[in] = struct{}{} + } + } + } + return res +} + +func (c Circuit) SetNbUniqueOutputs() { + + for i := range c { + c[i].NbUniqueOutputs = 0 + } + + curWireIn := make([]bool, len(c)) + uniqueIns := make([]int, 0, len(c)) + for i := range c { + // clear the caches + for j := range uniqueIns { + curWireIn[uniqueIns[j]] = false + } + uniqueIns = uniqueIns[:0] + + // count! + for _, in := range c[i].Inputs { + if !curWireIn[in] { + c[in].NbUniqueOutputs++ + curWireIn[in] = true + uniqueIns = append(uniqueIns, in) + } + } + } +} + +func (c Circuit) Inputs() []int { + res := make([]int, 0, len(c)) + for i := range c { + if c[i].IsInput() { + res = append(res, i) + } + } + return res +} + +func (c Circuit) MaxGateNbIn() int { + res := 0 + for i := range c { + res = max(res, len(c[i].Inputs)) + } + return res +} + +func CircuitInfoToCircuit(info gkrinfo.Circuit, gateGetter func(name gkr.GateName) *Gate) (Circuit, error) { + resCircuit := make(Circuit, len(info)) + for i := range info { + if info[i].Gate == "" && len(info[i].Inputs) == 0 { + continue + } + resCircuit[i].Inputs = info[i].Inputs + resCircuit[i].Gate = gateGetter(gkr.GateName(info[i].Gate)) + if resCircuit[i].Gate == nil { + return nil, fmt.Errorf("gate \"%s\" not found", info[i].Gate) + } + } + return resCircuit, nil +} + +func StoringToSolvingInfo(info gkrinfo.StoringInfo, gateGetter func(name gkr.GateName) *Gate) (SolvingInfo, error) { + circuit, err := CircuitInfoToCircuit(info.Circuit, gateGetter) + return SolvingInfo{ + Circuit: circuit, + NbInstances: info.NbInstances, + HashName: info.HashName, + Dependencies: info.Dependencies, + Prints: info.Prints, + }, err +} + +// WireAssignment is assignment of values to the same wire across many instances of the circuit +type WireAssignment []polynomial.MultiLin + +func (a WireAssignment) Permute(p gkrinfo.Permutations) { + utils.Permute(a, p.WiresPermutation) + for i := range a { + if a[i] != nil { + utils.Permute(a[i], p.InstancesPermutation) + } + } +} + +func (a WireAssignment) NbInstances() int { + for _, aW := range a { + if aW != nil { + return len(aW) + } + } + panic("empty assignment") +} + +func (a WireAssignment) NbVars() int { + for _, aW := range a { + if aW != nil { + return aW.NumVars() + } + } + panic("empty assignment") +} + +// ProofSize computes how large the proof for a circuit would be. It needs NbUniqueOutputs to be set. +func (c Circuit) ProofSize(logNbInstances int) int { + nbUniqueInputs := 0 + nbPartialEvalPolys := 0 + for i := range c { + nbUniqueInputs += c[i].NbUniqueOutputs // each unique output is manifest in a finalEvalProof entry + if !c[i].NoProof() { + nbPartialEvalPolys += c[i].Gate.Degree() + 1 + } + } + return nbUniqueInputs + nbPartialEvalPolys*logNbInstances +} + +// makeNeg1Slice returns a slice of size n with all elements set to -1. +func makeNeg1Slice(n int) []int { + res := make([]int, n) + for i := range res { + res[i] = -1 + } + return res +} + +type topSortData struct { + outputs [][]int + status []int // status > 0 indicates number of inputs left to be ready. status = 0 means ready. status = -1 means done + leastReady int +} + +func (d *topSortData) markDone(i int) { + + d.status[i] = -1 + + for _, outI := range d.outputs[i] { + d.status[outI]-- + if d.status[outI] == 0 && outI < d.leastReady { + d.leastReady = outI + } + } + + for d.leastReady < len(d.status) && d.status[d.leastReady] != 0 { + d.leastReady++ + } +} + +func statusList(c Circuit) []int { + res := make([]int, len(c)) + for i := range c { + res[i] = len(c[i].Inputs) + } + return res +} + +// TopologicalSort sorts the wires in order of dependence. Such that for any wire, any one it depends on +// occurs before it. It tries to stick to the input order as much as possible. An already sorted list will remain unchanged. +// It also sets the nbOutput flags, and a dummy IdentityGate for input wires. +// Worst-case inefficient O(n^2), but that probably won't matter since the circuits are small. +// Furthermore, it is efficient with already-close-to-sorted lists, which are the expected input +func (c Circuit) TopologicalSort() []*Wire { + var data topSortData + data.outputs = c.OutputsList() + data.status = statusList(c) + sorted := make([]*Wire, len(c)) + + for data.leastReady = 0; data.status[data.leastReady] != 0; data.leastReady++ { + } + + for i := range c { + sorted[i] = &c[data.leastReady] + data.markDone(data.leastReady) + } + + return sorted +} + +var ErrZeroFunction = errors.New("detected a zero function") + +// some sample gates + +// Identity gate: x -> x +func Identity() *Gate { + return NewGate(func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return in[0] + }, 1, 1, 0) +} + +// Add2 gate: (x, y) -> x + y +func Add2() *Gate { + return NewGate(func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.Add(in[0], in[1]) + }, 2, 1, 0) +} + +// Sub2 gate: (x, y) -> x - y +func Sub2() *Gate { + return NewGate(func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.Sub(in[0], in[1]) + }, 2, 1, 0) +} + +// Neg gate: x -> -x +func Neg() *Gate { + return NewGate(func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.Neg(in[0]) + }, 1, 1, 0) +} + +// Mul2 gate: (x, y) -> x * y +func Mul2() *Gate { + return NewGate(func(api gkr.GateAPI, in ...frontend.Variable) frontend.Variable { + return api.Mul(in[0], in[1]) + }, 2, 2, -1) +} diff --git a/internal/gkr/small_rational/gate_testing.go b/internal/gkr/small_rational/gate_testing.go new file mode 100644 index 00000000..dc29624d --- /dev/null +++ b/internal/gkr/small_rational/gate_testing.go @@ -0,0 +1,198 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + + "errors" + "slices" + + "github.com/consensys/gnark/internal/small_rational" + "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// IsGateFunctionAdditive returns whether x_i occurs only in a monomial of total degree 1 in f +func IsGateFunctionAdditive(f gkr.GateFunction, i, nbIn int) bool { + fWrapped := api.convertFunc(f) + + // fix all variables except the i-th one at random points + // pick random value x1 for the i-th variable + // check if f(-, 0, -) + f(-, 2*x1, -) = 2*f(-, x1, -) + x := make(small_rational.Vector, nbIn) + x.MustSetRandom() + x0 := x[i] + x[i].SetZero() + in := slices.Clone(x) + y0 := fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 := fWrapped(in...) + + x[i].Double(&x[i]) + copy(in, x) + y2 := fWrapped(in...) + + y2.Sub(y2, y1) + y1.Sub(y1, y0) + + if !y2.Equal(y1) { + return false // not linear + } + + // check if the coefficient of x_i is nonzero and independent of the other variables (so that we know it is ALWAYS nonzero) + if y1.IsZero() { // f(-, x1, -) = f(-, 0, -), so the coefficient of x_i is 0 + return false + } + + // compute the slope with another assignment for the other variables + x.MustSetRandom() + x[i].SetZero() + copy(in, x) + y0 = fWrapped(in...) + + x[i] = x0 + copy(in, x) + y1 = fWrapped(in...) + + y1.Sub(y1, y0) + + return y1.Equal(y2) +} + +// fitPoly tries to fit a polynomial of degree less than degreeBound to f. +// degreeBound must be a power of 2. +// It returns the polynomial if successful, nil otherwise +func (f gateFunctionFr) fitPoly(nbIn int, degreeBound uint64) polynomial.Polynomial { + // turn f univariate by defining p(x) as f(x, rx, ..., sx) + // where r, s, ... are random constants + fIn := make([]small_rational.SmallRational, nbIn) + consts := make(small_rational.Vector, nbIn-1) + consts.MustSetRandom() + + p := make(polynomial.Polynomial, degreeBound) + x := make(small_rational.Vector, degreeBound) + x.MustSetRandom() + for i := range x { + fIn[0] = x[i] + for j := range consts { + fIn[j+1].Mul(&x[i], &consts[j]) + } + p[i].Set(f(fIn...)) + } + + // obtain p's coefficients + p, err := interpolate(x, p) + if err != nil { + panic(err) + } + + // check if p is equal to f. This not being the case means that f is of a degree higher than degreeBound + fIn[0].MustSetRandom() + for i := range consts { + fIn[i+1].Mul(&fIn[0], &consts[i]) + } + pAt := p.Eval(&fIn[0]) + fAt := f(fIn...) + if !pAt.Equal(fAt) { + return nil + } + + // trim p + lastNonZero := len(p) - 1 + for lastNonZero >= 0 && p[lastNonZero].IsZero() { + lastNonZero-- + } + return p[:lastNonZero+1] +} + +// FindGateFunctionDegree returns the degree of the gate function, or -1 if it fails. +// Failure could be due to the degree being higher than max or the function not being a polynomial at all. +func FindGateFunctionDegree(f gkr.GateFunction, max, nbIn int) (int, error) { + fFr := api.convertFunc(f) + bound := uint64(max) + 1 + for degreeBound := uint64(4); degreeBound <= bound; degreeBound *= 8 { + if p := fFr.fitPoly(nbIn, degreeBound); p != nil { + if len(p) == 0 { + return -1, gkrtypes.ErrZeroFunction + } + return len(p) - 1, nil + } + } + return -1, fmt.Errorf("could not find a degree: tried up to %d", max) +} + +func VerifyGateFunctionDegree(f gkr.GateFunction, claimedDegree, nbIn int) error { + fFr := api.convertFunc(f) + if p := fFr.fitPoly(nbIn, ecc.NextPowerOfTwo(uint64(claimedDegree)+1)); p == nil { + return fmt.Errorf("detected a higher degree than %d", claimedDegree) + } else if len(p) == 0 { + return gkrtypes.ErrZeroFunction + } else if len(p)-1 != claimedDegree { + return fmt.Errorf("detected degree %d, claimed %d", len(p)-1, claimedDegree) + } + return nil +} + +// interpolate fits a polynomial of degree len(X) - 1 = len(Y) - 1 to the points (X[i], Y[i]) +// Note that the runtime is O(len(X)³) +func interpolate(X, Y []small_rational.SmallRational) (polynomial.Polynomial, error) { + if len(X) != len(Y) { + return nil, errors.New("X and Y must have the same length") + } + + // solve the system of equations by Gaussian elimination + augmentedRows := make([][]small_rational.SmallRational, len(X)) // the last column is the Y values + for i := range augmentedRows { + augmentedRows[i] = make([]small_rational.SmallRational, len(X)+1) + augmentedRows[i][0].SetOne() + augmentedRows[i][1].Set(&X[i]) + for j := 2; j < len(augmentedRows[i])-1; j++ { + augmentedRows[i][j].Mul(&augmentedRows[i][j-1], &X[i]) + } + augmentedRows[i][len(augmentedRows[i])-1].Set(&Y[i]) + } + + // make the upper triangle + for i := range len(augmentedRows) - 1 { + // use row i to eliminate the ith element in all rows below + var negInv small_rational.SmallRational + if augmentedRows[i][i].IsZero() { + return nil, errors.New("singular matrix") + } + negInv.Inverse(&augmentedRows[i][i]) + negInv.Neg(&negInv) + for j := i + 1; j < len(augmentedRows); j++ { + var c small_rational.SmallRational + c.Mul(&augmentedRows[j][i], &negInv) + // augmentedRows[j][i].SetZero() omitted + for k := i + 1; k < len(augmentedRows[i]); k++ { + var t small_rational.SmallRational + t.Mul(&augmentedRows[i][k], &c) + augmentedRows[j][k].Add(&augmentedRows[j][k], &t) + } + } + } + + // back substitution + res := make(polynomial.Polynomial, len(X)) + for i := len(augmentedRows) - 1; i >= 0; i-- { + res[i] = augmentedRows[i][len(augmentedRows[i])-1] + for j := i + 1; j < len(augmentedRows[i])-1; j++ { + var t small_rational.SmallRational + t.Mul(&res[j], &augmentedRows[i][j]) + res[i].Sub(&res[i], &t) + } + res[i].Div(&res[i], &augmentedRows[i][i]) + } + + return res, nil +} diff --git a/internal/gkr/small_rational/gkr.go b/internal/gkr/small_rational/gkr.go new file mode 100644 index 00000000..e8e78f4b --- /dev/null +++ b/internal/gkr/small_rational/gkr.go @@ -0,0 +1,816 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "fmt" + "math/big" + "strconv" + "sync" + + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/internal/small_rational" + "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +// The goal is to prove/verify evaluations of many instances of the same circuit + +// WireAssignment is assignment of values to the same wire across many instances of the circuit +type WireAssignment []polynomial.MultiLin + +type Proof []sumcheckProof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) + +// eqTimesGateEvalSumcheckLazyClaims is a lazy claim for sumcheck (verifier side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the checking of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckLazyClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]small_rational.SmallRational // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []small_rational.SmallRational // yᵢ = w(xᵢ), allegedly + manager *claimsManager // WARNING: Circular references +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) getWire() *gkrtypes.Wire { + return e.manager.wires[e.wireI] +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) claimsNum() int { + return len(e.evaluationPoints) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) varsNum() int { + return len(e.evaluationPoints[0]) +} + +// combinedSum returns ∑ᵢ aⁱ yᵢ +func (e *eqTimesGateEvalSumcheckLazyClaims) combinedSum(a small_rational.SmallRational) small_rational.SmallRational { + evalsAsPoly := polynomial.Polynomial(e.claimedEvaluations) + return evalsAsPoly.Eval(&a) +} + +func (e *eqTimesGateEvalSumcheckLazyClaims) degree(int) int { + return 1 + e.manager.wires[e.wireI].Gate.Degree() +} + +// verifyFinalEval finalizes the verification of w. +// The prover's claims w(xᵢ) = yᵢ have already been reduced to verifying +// ∑ cⁱ eq(xᵢ, r) w(r) = purportedValue. ( c is combinationCoeff ) +// Both purportedValue and the vector r have been randomized during the sumcheck protocol. +// By taking the w term out of the sum we get the equivalent claim that +// for E := ∑ eq(xᵢ, r), it must be that E w(r) = purportedValue. +// If w is an input wire, the verifier can directly check its evaluation at r. +// Otherwise, the prover makes claims about the evaluation of w's input wires, +// wᵢ, at r, to be verified later. +// The claims are communicated through the proof parameter. +// The verifier checks here if the claimed evaluations of wᵢ(r) are consistent with +// the main claim, by checking E w(wᵢ(r)...) = purportedValue. +func (e *eqTimesGateEvalSumcheckLazyClaims) verifyFinalEval(r []small_rational.SmallRational, combinationCoeff, purportedValue small_rational.SmallRational, uniqueInputEvaluations []small_rational.SmallRational) error { + // the eq terms ( E ) + numClaims := len(e.evaluationPoints) + evaluation := polynomial.EvalEq(e.evaluationPoints[numClaims-1], r) + for i := numClaims - 2; i >= 0; i-- { + evaluation.Mul(&evaluation, &combinationCoeff) + eq := polynomial.EvalEq(e.evaluationPoints[i], r) + evaluation.Add(&evaluation, &eq) + } + + wire := e.manager.wires[e.wireI] + + // the w(...) term + var gateEvaluation small_rational.SmallRational + if wire.IsInput() { // just compute w(r) + gateEvaluation = e.manager.assignment[e.wireI].Evaluate(r, e.manager.memPool) + } else { // proof contains the evaluations of the inputs, but avoids repetition in case multiple inputs come from the same wire + injection, injectionLeftInv := + e.manager.wires.ClaimPropagationInfo(e.wireI) + + if len(injection) != len(uniqueInputEvaluations) { + return fmt.Errorf("%d input wire evaluations given, %d expected", len(uniqueInputEvaluations), len(injection)) + } + + for uniqueI, i := range injection { // map from unique to all + e.manager.add(wire.Inputs[i], r, uniqueInputEvaluations[uniqueI]) + } + + inputEvaluations := make([]frontend.Variable, len(wire.Inputs)) + for i, uniqueI := range injectionLeftInv { // map from all to unique + inputEvaluations[i] = &uniqueInputEvaluations[uniqueI] + } + + gateEvaluation.Set(wire.Gate.Evaluate(api, inputEvaluations...).(*small_rational.SmallRational)) + } + + evaluation.Mul(&evaluation, &gateEvaluation) + + if evaluation.Equal(&purportedValue) { + return nil + } + return errors.New("incompatible evaluations") +} + +// eqTimesGateEvalSumcheckClaims is a claim for sumcheck (prover side). +// eqTimesGateEval is a polynomial consisting of ∑ᵢ cⁱ eq(-, xᵢ) w(-). +// Its purpose is to batch the proving of multiple evaluations of the same wire. +type eqTimesGateEvalSumcheckClaims struct { + wireI int // the wire for which we are making the claim, with value w + evaluationPoints [][]small_rational.SmallRational // xᵢ: the points at which the prover has made claims about the evaluation of w + claimedEvaluations []small_rational.SmallRational // yᵢ = w(xᵢ) + manager *claimsManager + + input []polynomial.MultiLin // input[i](h₁, ..., hₘ₋ⱼ) = wᵢ(r₁, r₂, ..., rⱼ₋₁, h₁, ..., hₘ₋ⱼ) + + eq polynomial.MultiLin // E := ∑ᵢ cⁱ eq(xᵢ, -) +} + +func (c *eqTimesGateEvalSumcheckClaims) getWire() *gkrtypes.Wire { + return c.manager.wires[c.wireI] +} + +// combine the multiple claims into one claim using a random combination (combinationCoeff or c). +// From the original multiple claims of w(xᵢ) = yᵢ, we get a single claim +// ∑ᵢ,ₕ cⁱ eq(xᵢ, h) w(h) = ∑ᵢ cⁱ yᵢ, where h iterates over the hypercube (circuit instances) and +// i iterates over the claims. +// Equivalently, we could say ∑ᵢ cⁱ yᵢ = ∑ₕ,ᵢ cⁱ eq(xᵢ, h) w(h) = ∑ₕ w(h) ∑ᵢ cⁱ eq(xᵢ, h). +// Thus if we initially compute E := ∑ᵢ cⁱ eq(xᵢ, -), our claim will find the simpler form +// ∑ᵢ cⁱ yᵢ = ∑ₕ w(h) E(h), where the sum-checked polynomial is of degree deg(g) + 1, +// and deg(g) is the total degree of the polynomial defining the gate g of which w is the output. +// The output of combine is the first sumcheck claim, i.e. ∑₍ₕ₁,ₕ₂,...₎ w(X, h₁, h₂, ...) E(X, h₁, h₂, ...).. +func (c *eqTimesGateEvalSumcheckClaims) combine(combinationCoeff small_rational.SmallRational) polynomial.Polynomial { + varsNum := c.varsNum() + eqLength := 1 << varsNum + claimsNum := c.claimsNum() + // initialize the eq tables ( E ) + c.eq = c.manager.memPool.Make(eqLength) + + c.eq[0].SetOne() + c.eq.Eq(c.evaluationPoints[0]) + + // E := eq(x₀, -) + newEq := polynomial.MultiLin(c.manager.memPool.Make(eqLength)) + aI := combinationCoeff + + // E += cⁱ eq(xᵢ, -) + for k := 1; k < claimsNum; k++ { + newEq[0].Set(&aI) + + c.eqAcc(c.eq, newEq, c.evaluationPoints[k]) + + if k+1 < claimsNum { + aI.Mul(&aI, &combinationCoeff) + } + } + + c.manager.memPool.Dump(newEq) + + return c.computeGJ() +} + +// eqAcc sets m to an eq table at q and then adds it to e. +// m <- eq(q, -). +// e <- e + m +func (c *eqTimesGateEvalSumcheckClaims) eqAcc(e, m polynomial.MultiLin, q []small_rational.SmallRational) { + n := len(q) + + //At the end of each iteration, m(h₁, ..., hₙ) = eq(q₁, ..., qᵢ₊₁, h₁, ..., hᵢ₊₁) + for i := range q { // In the comments we use a 1-based index so q[i] = qᵢ₊₁ + // go through all assignments of (b₁, ..., bᵢ) ∈ {0,1}ⁱ + const threshold = 1 << 6 + k := 1 << i + if k < threshold { + for j := 0; j < k; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + } else { + c.manager.workers.Submit(k, func(start, end int) { + for j := start; j < end; j++ { + j0 := j << (n - i) // bᵢ₊₁ = 0 + j1 := j0 + 1<<(n-1-i) // bᵢ₊₁ = 1 + + m[j1].Mul(&q[i], &m[j0]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 1) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) qᵢ₊₁ + m[j0].Sub(&m[j0], &m[j1]) // eq(q₁, ..., qᵢ₊₁, b₁, ..., bᵢ, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) eq(qᵢ₊₁, 0) = eq(q₁, ..., qᵢ, b₁, ..., bᵢ) (1-qᵢ₊₁) + } + }, 1024).Wait() + } + + } + c.manager.workers.Submit(len(e), func(start, end int) { + for i := start; i < end; i++ { + e[i].Add(&e[i], &m[i]) + } + }, 512).Wait() +} + +// computeGJ: gⱼ = ∑_{0≤h<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, h...) = ∑_{0≤i<2ⁿ⁻ʲ} E(r₁, ..., Xⱼ, h...) g( w₀(r₁, ..., Xⱼ, h...), ... ). +// the polynomial is represented by the evaluations gⱼ(1), gⱼ(2), ..., gⱼ(deg(gⱼ)). +// The value gⱼ(0) is inferred from the equation gⱼ(0) + gⱼ(1) = gⱼ₋₁(rⱼ₋₁). By convention, g₀ is a constant polynomial equal to the claimed sum. +func (c *eqTimesGateEvalSumcheckClaims) computeGJ() polynomial.Polynomial { + + wire := c.getWire() + degGJ := 1 + wire.Gate.Degree() // guaranteed to be no smaller than the actual deg(gⱼ) + nbGateIn := len(c.input) + + // Both E and wᵢ (the input wires and the eq table) are multilinear, thus + // they are linear in Xⱼ. + // So for f ∈ { E(r₁, ..., Xⱼ, h...) } ∪ {wᵢ(r₁, ..., Xⱼ, h...) }, so f(m) = m×(f(1) - f(0)) + f(0), and f(0), f(1) are easily computed from the bookkeeping tables. + // ml are such multilinear polynomials the evaluations of which over different values of Xⱼ are computed in this stepwise manner. + ml := make([]polynomial.MultiLin, nbGateIn+1) // shortcut to the evaluations of the multilinear polynomials over the hypercube + ml[0] = c.eq + copy(ml[1:], c.input) + + sumSize := len(c.eq) / 2 // the range of h, over which we sum + + // Perf-TODO: Collate once at claim "combination" time and not again. then, even folding can be done in one operation every time "next" is called + + gJ := make([]small_rational.SmallRational, degGJ) + var mu sync.Mutex + computeAll := func(start, end int) { // compute method to allow parallelization across instances + var step small_rational.SmallRational + + res := make([]small_rational.SmallRational, degGJ) + + // evaluations of ml, laid out as: + // ml[0](1, h...), ml[1](1, h...), ..., ml[len(ml)-1](1, h...), + // ml[0](2, h...), ml[1](2, h...), ..., ml[len(ml)-1](2, h...), + // ... + // ml[0](degGJ, h...), ml[2](degGJ, h...), ..., ml[len(ml)-1](degGJ, h...) + mlEvals := make([]small_rational.SmallRational, degGJ*len(ml)) + gateInput := make([]frontend.Variable, nbGateIn) + + for h := start; h < end; h++ { // h counts across instances + + evalAt1Index := sumSize + h + for k := range ml { + // d = 0 + mlEvals[k].Set(&ml[k][evalAt1Index]) // evaluation at Xⱼ = 1. Can be taken directly from the table. + step.Sub(&mlEvals[k], &ml[k][h]) // step = ml[k](1) - ml[k](0) + for d := 1; d < degGJ; d++ { + mlEvals[d*len(ml)+k].Add(&mlEvals[(d-1)*len(ml)+k], &step) + } + } + + eIndex := 0 // index for where the current eq term is + nextEIndex := len(ml) + for d := range degGJ { + for i := range gateInput { + gateInput[i] = &mlEvals[eIndex+1+i] + } + summand := wire.Gate.Evaluate(api, gateInput...).(*small_rational.SmallRational) + summand.Mul(summand, &mlEvals[eIndex]) + res[d].Add(&res[d], summand) // collect contributions into the sum from start to end + eIndex, nextEIndex = nextEIndex, nextEIndex+len(ml) + } + } + mu.Lock() + for i := range gJ { + gJ[i].Add(&gJ[i], &res[i]) // collect into the complete sum + } + mu.Unlock() + } + + const minBlockSize = 64 + + if sumSize < minBlockSize { + // no parallelization + computeAll(0, sumSize) + } else { + c.manager.workers.Submit(sumSize, computeAll, minBlockSize).Wait() + } + + return gJ +} + +// next first folds the input and E polynomials at the given verifier challenge then computes the new gⱼ. +// Thus, j <- j+1 and rⱼ = challenge. +func (c *eqTimesGateEvalSumcheckClaims) next(challenge small_rational.SmallRational) polynomial.Polynomial { + const minBlockSize = 512 + n := len(c.eq) / 2 + if n < minBlockSize { + // no parallelization + for i := 0; i < len(c.input); i++ { + c.input[i].Fold(challenge) + } + c.eq.Fold(challenge) + } else { + wgs := make([]*sync.WaitGroup, len(c.input)) + for i := 0; i < len(c.input); i++ { + wgs[i] = c.manager.workers.Submit(n, c.input[i].FoldParallel(challenge), minBlockSize) + } + c.manager.workers.Submit(n, c.eq.FoldParallel(challenge), minBlockSize).Wait() + for _, wg := range wgs { + wg.Wait() + } + } + + return c.computeGJ() +} + +func (c *eqTimesGateEvalSumcheckClaims) varsNum() int { + return len(c.evaluationPoints[0]) +} + +func (c *eqTimesGateEvalSumcheckClaims) claimsNum() int { + return len(c.claimedEvaluations) +} + +// proveFinalEval provides the values wᵢ(r₁, ..., rₙ) +func (c *eqTimesGateEvalSumcheckClaims) proveFinalEval(r []small_rational.SmallRational) []small_rational.SmallRational { + //defer the proof, return list of claims + + injection, _ := c.manager.wires.ClaimPropagationInfo(c.wireI) // TODO @Tabaie: Instead of doing this last, we could just have fewer input in the first place; not that likely to happen with single gates, but more so with layers. + evaluations := make([]small_rational.SmallRational, len(injection)) + for i, gateInputI := range injection { + wI := c.input[gateInputI] + wI.Fold(r[len(r)-1]) // We already have wᵢ(r₁, ..., rₙ₋₁, hₙ) in a table. Only one more fold required. + c.manager.add(c.getWire().Inputs[gateInputI], r, wI[0]) + evaluations[i] = wI[0] + } + + c.manager.memPool.Dump(c.claimedEvaluations, c.eq) + + return evaluations +} + +type claimsManager struct { + claims []*eqTimesGateEvalSumcheckLazyClaims + assignment WireAssignment + memPool *polynomial.Pool + workers *utils.WorkerPool + wires gkrtypes.Wires +} + +func newClaimsManager(wires []*gkrtypes.Wire, assignment WireAssignment, o settings) (manager claimsManager) { + manager.assignment = assignment + manager.claims = make([]*eqTimesGateEvalSumcheckLazyClaims, len(wires)) + manager.memPool = o.pool + manager.workers = o.workers + manager.wires = wires + + for i, wire := range wires { + + manager.claims[i] = &eqTimesGateEvalSumcheckLazyClaims{ + wireI: i, + evaluationPoints: make([][]small_rational.SmallRational, 0, wire.NbClaims()), + claimedEvaluations: manager.memPool.Make(wire.NbClaims()), + manager: &manager, + } + } + return +} + +func (m *claimsManager) add(wire int, evaluationPoint []small_rational.SmallRational, evaluation small_rational.SmallRational) { + claim := m.claims[wire] + i := len(claim.evaluationPoints) + claim.claimedEvaluations[i] = evaluation + claim.evaluationPoints = append(claim.evaluationPoints, evaluationPoint) +} + +func (m *claimsManager) getLazyClaim(wire int) *eqTimesGateEvalSumcheckLazyClaims { + return m.claims[wire] +} + +func (m *claimsManager) getClaim(wireI int) *eqTimesGateEvalSumcheckClaims { + lazy := m.claims[wireI] + wire := m.wires[wireI] + res := &eqTimesGateEvalSumcheckClaims{ + wireI: wireI, + evaluationPoints: lazy.evaluationPoints, + claimedEvaluations: lazy.claimedEvaluations, + manager: m, + } + + if wire.IsInput() { + res.input = []polynomial.MultiLin{m.memPool.Clone(m.assignment[wireI])} + } else { + res.input = make([]polynomial.MultiLin, len(wire.Inputs)) + + for inputI, inputW := range wire.Inputs { + res.input[inputI] = m.memPool.Clone(m.assignment[inputW]) //will be edited later, so must be deep copied + } + } + return res +} + +func (m *claimsManager) deleteClaim(wire int) { + m.claims[wire].manager = nil + m.claims[wire] = nil +} + +type settings struct { + pool *polynomial.Pool + sorted []*gkrtypes.Wire + transcript *fiatshamir.Transcript + transcriptPrefix string + nbVars int + workers *utils.WorkerPool +} + +type Option func(*settings) + +func WithPool(pool *polynomial.Pool) Option { + return func(options *settings) { + options.pool = pool + } +} + +func WithSortedCircuit(sorted []*gkrtypes.Wire) Option { + return func(options *settings) { + options.sorted = sorted + } +} + +func WithWorkers(workers *utils.WorkerPool) Option { + return func(options *settings) { + options.workers = workers + } +} + +func setup(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (settings, error) { + var o settings + var err error + for _, option := range options { + option(&o) + } + + o.nbVars = assignment.NumVars() + nbInstances := assignment.NumInstances() + if 1< 1 { //combine the claims + size++ + } + size += logNbInstances // full run of sumcheck on logNbInstances variables + } + + nums := make([]string, max(len(sorted), logNbInstances)) + for i := range nums { + nums[i] = strconv.Itoa(i) + } + + challenges := make([]string, size) + + // output wire claims + firstChallengePrefix := prefix + "fC." + for j := 0; j < logNbInstances; j++ { + challenges[j] = firstChallengePrefix + nums[j] + } + j := logNbInstances + for i := len(sorted) - 1; i >= 0; i-- { + if sorted[i].NoProof() { + continue + } + wirePrefix := prefix + "w" + nums[i] + "." + + if sorted[i].NbClaims() > 1 { + challenges[j] = wirePrefix + "comb" + j++ + } + + partialSumPrefix := wirePrefix + "pSP." + for k := 0; k < logNbInstances; k++ { + challenges[j] = partialSumPrefix + nums[k] + j++ + } + } + return challenges +} + +func getFirstChallengeNames(logNbInstances int, prefix string) []string { + res := make([]string, logNbInstances) + firstChallengePrefix := prefix + "fC." + for i := 0; i < logNbInstances; i++ { + res[i] = firstChallengePrefix + strconv.Itoa(i) + } + return res +} + +func getChallenges(transcript *fiatshamir.Transcript, names []string) ([]small_rational.SmallRational, error) { + res := make([]small_rational.SmallRational, len(names)) + for i, name := range names { + if bytes, err := transcript.ComputeChallenge(name); err == nil { + res[i].SetBytes(bytes) + } else { + return nil, err + } + } + return res, nil +} + +// Prove consistency of the claimed assignment +func Prove(c gkrtypes.Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (Proof, error) { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return nil, err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + proof := make(Proof, len(c)) + // firstChallenge called rho in the paper + var firstChallenge []small_rational.SmallRational + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return nil, err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + claim := claims.getClaim(i) + if wire.NoProof() { // input wires with one claim only + proof[i] = sumcheckProof{ + partialSumPolys: []polynomial.Polynomial{}, + finalEvalProof: []small_rational.SmallRational{}, + } + } else { + if proof[i], err = sumcheckProve( + claim, fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err != nil { + return proof, err + } + + baseChallenge = make([][]byte, len(proof[i].finalEvalProof)) + for j := range proof[i].finalEvalProof { + baseChallenge[j] = proof[i].finalEvalProof[j].Marshal() + } + } + // the verifier checks a single claim about input wires itself + claims.deleteClaim(i) + } + + return proof, nil +} + +// Verify the consistency of the claimed output with the claimed input +// Unlike in Prove, the assignment argument need not be complete +func Verify(c gkrtypes.Circuit, assignment WireAssignment, proof Proof, transcriptSettings fiatshamir.Settings, options ...Option) error { + o, err := setup(c, assignment, transcriptSettings, options...) + if err != nil { + return err + } + defer o.workers.Stop() + + claims := newClaimsManager(o.sorted, assignment, o) + + var firstChallenge []small_rational.SmallRational + firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) + if err != nil { + return err + } + + wirePrefix := o.transcriptPrefix + "w" + var baseChallenge [][]byte + for i := len(c) - 1; i >= 0; i-- { + wire := o.sorted[i] + + if wire.IsOutput() { + claims.add(i, firstChallenge, assignment[i].Evaluate(firstChallenge, claims.memPool)) + } + + proofW := proof[i] + claim := claims.getLazyClaim(i) + if wire.NoProof() { // input wires with one claim only + // make sure the proof is empty + if len(proofW.finalEvalProof) != 0 || len(proofW.partialSumPolys) != 0 { + return errors.New("no proof allowed for input wire with a single claim") + } + + if wire.NbClaims() == 1 { // input wire + // simply evaluate and see if it matches + evaluation := assignment[i].Evaluate(claim.evaluationPoints[0], claims.memPool) + if !claim.claimedEvaluations[0].Equal(&evaluation) { + return errors.New("incorrect input wire claim") + } + } + } else if err = sumcheckVerify( + claim, proof[i], fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), + ); err == nil { // incorporate prover claims about w's input into the transcript + baseChallenge = make([][]byte, len(proofW.finalEvalProof)) + for j := range baseChallenge { + baseChallenge[j] = proofW.finalEvalProof[j].Marshal() + } + } else { + return fmt.Errorf("sumcheck proof rejected: %v", err) //TODO: Any polynomials to dump? + } + claims.deleteClaim(i) + } + return nil +} + +// Complete the circuit evaluation from input values +func (a WireAssignment) Complete(wires gkrtypes.Wires) WireAssignment { + + nbInstances := a.NumInstances() + maxNbIns := 0 + + for i, w := range wires { + maxNbIns = max(maxNbIns, len(w.Inputs)) + if len(a[i]) != nbInstances { + a[i] = make([]small_rational.SmallRational, nbInstances) + } + } + + ins := make([]small_rational.SmallRational, maxNbIns) + for i := range nbInstances { + for wI, w := range wires { + if !w.IsInput() { + for inI, in := range w.Inputs { + ins[inI] = a[in][i] + } + a[wI][i].Set(api.evaluate(w.Gate.Evaluate, ins[:len(w.Inputs)]...)) + } + } + } + + return a +} + +func (a WireAssignment) NumInstances() int { + for _, aW := range a { + return len(aW) + } + panic("empty assignment") +} + +func (a WireAssignment) NumVars() int { + for _, aW := range a { + return aW.NumVars() + } + panic("empty assignment") +} + +// SerializeToBigInts flattens a proof object into the given slice of big.Ints +// useful in gnark hints. +func (p Proof) SerializeToBigInts(outs []*big.Int) error { + offset := 0 + for i := range p { + for _, poly := range p[i].partialSumPolys { + frToBigInts(outs[offset:], poly) + offset += len(poly) + } + if p[i].finalEvalProof != nil { + frToBigInts(outs[offset:], p[i].finalEvalProof) + offset += len(p[i].finalEvalProof) + } + } + if offset != len(outs) { + return fmt.Errorf("expected %d elements, got %d", offset, len(outs)) + } + return nil +} + +func frToBigInts(dst []*big.Int, src []small_rational.SmallRational) { + for i := range src { + src[i].BigInt(dst[i]) + } +} + +// gateAPI implements gkr.GateAPI. +type gateAPI struct{} + +var api gateAPI + +func (gateAPI) Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res small_rational.SmallRational // TODO Heap allocated. Keep an eye on perf + res.Add(cast(i1), cast(i2)) + for _, v := range in { + res.Add(&res, cast(v)) + } + return &res +} + +func (gateAPI) MulAcc(a, b, c frontend.Variable) frontend.Variable { + var prod small_rational.SmallRational + prod.Add(cast(b), cast(c)) + res := cast(a) + res.Add(res, &prod) + return &res +} + +func (gateAPI) Neg(i1 frontend.Variable) frontend.Variable { + var res small_rational.SmallRational + res.Neg(cast(i1)) + return &res +} + +func (gateAPI) Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res small_rational.SmallRational + res.Sub(cast(i1), cast(i2)) + for _, v := range in { + res.Sub(&res, cast(v)) + } + return &res +} + +func (gateAPI) Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable { + var res small_rational.SmallRational + res.Mul(cast(i1), cast(i2)) + for _, v := range in { + res.Mul(&res, cast(v)) + } + return &res +} + +func (gateAPI) Println(a ...frontend.Variable) { + toPrint := make([]any, len(a)) + var x small_rational.SmallRational + + for i, v := range a { + if _, err := x.SetInterface(v); err != nil { + toPrint[i] = x.String() + } else { + if s, ok := v.(string); ok { + toPrint[i] = s + continue + } + panic(fmt.Errorf("not numeric or string: %w", err)) + } + } + fmt.Println(toPrint...) +} + +func (api gateAPI) evaluate(f gkr.GateFunction, in ...small_rational.SmallRational) *small_rational.SmallRational { + inVar := make([]frontend.Variable, len(in)) + for i := range in { + inVar[i] = &in[i] + } + return f(api, inVar...).(*small_rational.SmallRational) +} + +type gateFunctionFr func(...small_rational.SmallRational) *small_rational.SmallRational + +// convertFunc turns f into a function that accepts and returns small_rational.SmallRational. +func (api gateAPI) convertFunc(f gkr.GateFunction) gateFunctionFr { + return func(in ...small_rational.SmallRational) *small_rational.SmallRational { + return api.evaluate(f, in...) + } +} + +func cast(v frontend.Variable) *small_rational.SmallRational { + if x, ok := v.(*small_rational.SmallRational); ok { // fast path, no extra heap allocation + return x + } + var x small_rational.SmallRational + if _, err := x.SetInterface(v); err != nil { + panic(err) + } + return &x +} diff --git a/internal/gkr/small_rational/sumcheck.go b/internal/gkr/small_rational/sumcheck.go new file mode 100644 index 00000000..60c88390 --- /dev/null +++ b/internal/gkr/small_rational/sumcheck.go @@ -0,0 +1,171 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "errors" + "strconv" + + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark/internal/small_rational" + "github.com/consensys/gnark/internal/small_rational/polynomial" +) + +// This does not make use of parallelism and represents polynomials as lists of coefficients +// It is currently geared towards arithmetic hashes. Once we have a more unified hash function interface, this can be generified. + +// sumcheckClaims to a multi-sumcheck statement. i.e. one of the form ∑_{0≤i<2ⁿ} fⱼ(i) = cⱼ for 1 ≤ j ≤ m. +// Later evolving into a claim of the form gⱼ = ∑_{0≤i<2ⁿ⁻ʲ} g(r₁, r₂, ..., rⱼ₋₁, Xⱼ, i...) +type sumcheckClaims interface { + combine(a small_rational.SmallRational) polynomial.Polynomial // combine into the 0ᵗʰ sumcheck subclaim. Create g := ∑_{1≤j≤m} aʲ⁻¹fⱼ for which now we seek to prove ∑_{0≤i<2ⁿ} g(i) = c := ∑_{1≤j≤m} aʲ⁻¹cⱼ. Return g₁. + next(small_rational.SmallRational) polynomial.Polynomial // Return the evaluations gⱼ(k) for 1 ≤ k < degⱼ(g). Update the claim to gⱼ₊₁ for the input value as rⱼ + varsNum() int // number of variables + claimsNum() int // number of claims + proveFinalEval(r []small_rational.SmallRational) []small_rational.SmallRational // in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +// sumcheckLazyClaims is the sumcheckClaims data structure on the verifier side. It is "lazy" in that it has to compute fewer things. +type sumcheckLazyClaims interface { + claimsNum() int // claimsNum = m + varsNum() int // varsNum = n + combinedSum(a small_rational.SmallRational) small_rational.SmallRational // combinedSum returns c = ∑_{1≤j≤m} aʲ⁻¹cⱼ + degree(i int) int // degree of the total claim in the i'th variable + verifyFinalEval(r []small_rational.SmallRational, combinationCoeff small_rational.SmallRational, purportedValue small_rational.SmallRational, proof []small_rational.SmallRational) error +} + +// sumcheckProof of a multi-statement. +type sumcheckProof struct { + partialSumPolys []polynomial.Polynomial + finalEvalProof []small_rational.SmallRational //in case it is difficult for the verifier to compute g(r₁, ..., rₙ) on its own, the prover can provide the value and a proof +} + +func setupTranscript(claimsNum int, varsNum int, settings *fiatshamir.Settings) (challengeNames []string, err error) { + numChallenges := varsNum + if claimsNum >= 2 { + numChallenges++ + } + challengeNames = make([]string, numChallenges) + if claimsNum >= 2 { + challengeNames[0] = settings.Prefix + "comb" + } + prefix := settings.Prefix + "pSP." + for i := 0; i < varsNum; i++ { + challengeNames[i+numChallenges-varsNum] = prefix + strconv.Itoa(i) + } + if settings.Transcript == nil { + transcript := fiatshamir.NewTranscript(settings.Hash, challengeNames...) + settings.Transcript = transcript + } + + for i := range settings.BaseChallenges { + if err = settings.Transcript.Bind(challengeNames[0], settings.BaseChallenges[i]); err != nil { + return + } + } + return +} + +func next(transcript *fiatshamir.Transcript, bindings []small_rational.SmallRational, remainingChallengeNames *[]string) (small_rational.SmallRational, error) { + challengeName := (*remainingChallengeNames)[0] + for i := range bindings { + bytes := bindings[i].Bytes() + if err := transcript.Bind(challengeName, bytes[:]); err != nil { + return small_rational.SmallRational{}, err + } + } + var res small_rational.SmallRational + bytes, err := transcript.ComputeChallenge(challengeName) + res.SetBytes(bytes) + + *remainingChallengeNames = (*remainingChallengeNames)[1:] + + return res, err +} + +// sumcheckProve create a non-interactive proof +func sumcheckProve(claims sumcheckClaims, transcriptSettings fiatshamir.Settings) (sumcheckProof, error) { + + var proof sumcheckProof + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return proof, err + } + + var combinationCoeff small_rational.SmallRational + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []small_rational.SmallRational{}, &remainingChallengeNames); err != nil { + return proof, err + } + } + + varsNum := claims.varsNum() + proof.partialSumPolys = make([]polynomial.Polynomial, varsNum) + proof.partialSumPolys[0] = claims.combine(combinationCoeff) + challenges := make([]small_rational.SmallRational, varsNum) + + for j := 0; j+1 < varsNum; j++ { + if challenges[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return proof, err + } + proof.partialSumPolys[j+1] = claims.next(challenges[j]) + } + + if challenges[varsNum-1], err = next(transcript, proof.partialSumPolys[varsNum-1], &remainingChallengeNames); err != nil { + return proof, err + } + + proof.finalEvalProof = claims.proveFinalEval(challenges) + + return proof, nil +} + +func sumcheckVerify(claims sumcheckLazyClaims, proof sumcheckProof, transcriptSettings fiatshamir.Settings) error { + remainingChallengeNames, err := setupTranscript(claims.claimsNum(), claims.varsNum(), &transcriptSettings) + transcript := transcriptSettings.Transcript + if err != nil { + return err + } + + var combinationCoeff small_rational.SmallRational + + if claims.claimsNum() >= 2 { + if combinationCoeff, err = next(transcript, []small_rational.SmallRational{}, &remainingChallengeNames); err != nil { + return err + } + } + + r := make([]small_rational.SmallRational, claims.varsNum()) + + // Just so that there is enough room for gJ to be reused + maxDegree := claims.degree(0) + for j := 1; j < claims.varsNum(); j++ { + if d := claims.degree(j); d > maxDegree { + maxDegree = d + } + } + gJ := make(polynomial.Polynomial, maxDegree+1) //At the end of iteration j, gJ = ∑_{i < 2ⁿ⁻ʲ⁻¹} g(X₁, ..., Xⱼ₊₁, i...) NOTE: n is shorthand for claims.varsNum() + gJR := claims.combinedSum(combinationCoeff) // At the beginning of iteration j, gJR = ∑_{i < 2ⁿ⁻ʲ} g(r₁, ..., rⱼ, i...) + + for j := range claims.varsNum() { + if len(proof.partialSumPolys[j]) != claims.degree(j) { + return errors.New("malformed proof") + } + copy(gJ[1:], proof.partialSumPolys[j]) + gJ[0].Sub(&gJR, &proof.partialSumPolys[j][0]) // Requirement that gⱼ(0) + gⱼ(1) = gⱼ₋₁(r) + // gJ is ready + + //Prepare for the next iteration + if r[j], err = next(transcript, proof.partialSumPolys[j], &remainingChallengeNames); err != nil { + return err + } + // This is an extremely inefficient way of interpolating. TODO: Interpolate without symbolically computing a polynomial + gJCoeffs := polynomial.InterpolateOnRange(gJ[:(claims.degree(j) + 1)]) + gJR = gJCoeffs.Eval(&r[j]) + } + + return claims.verifyFinalEval(r, combinationCoeff, gJR, proof.finalEvalProof) +} diff --git a/internal/gkr/small_rational/sumcheck_test.go b/internal/gkr/small_rational/sumcheck_test.go new file mode 100644 index 00000000..0d5a4806 --- /dev/null +++ b/internal/gkr/small_rational/sumcheck_test.go @@ -0,0 +1,86 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/stretchr/testify/assert" + + "strings" + "testing" +) + +func testSumcheckSingleClaimMultilin(polyInt []uint64, hashGenerator func() hash.Hash) error { + poly := make(polynomial.MultiLin, len(polyInt)) + for i, n := range polyInt { + poly[i].SetUint64(n) + } + + claim := singleMultilinClaim{g: poly.Clone()} + + proof, err := sumcheckProve(&claim, fiatshamir.WithHash(hashGenerator())) + if err != nil { + return err + } + + var sb strings.Builder + for _, p := range proof.partialSumPolys { + + sb.WriteString("\t{") + for i := 0; i < len(p); i++ { + sb.WriteString(p[i].String()) + if i+1 < len(p) { + sb.WriteString(", ") + } + } + sb.WriteString("}\n") + } + + lazyClaim := singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if err = sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())); err != nil { + return err + } + + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + lazyClaim = singleMultilinLazyClaim{g: poly, claimedSum: poly.Sum()} + if sumcheckVerify(lazyClaim, proof, fiatshamir.WithHash(hashGenerator())) == nil { + return fmt.Errorf("bad proof accepted") + } + return nil +} + +func TestSumcheckDeterministicHashSingleClaimMultilin(t *testing.T) { + + polys := [][]uint64{ + {1, 2, 3, 4}, // 1 + 2X₁ + X₂ + {1, 2, 3, 4, 5, 6, 7, 8}, // 1 + 4X₁ + 2X₂ + X₃ + {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}, // 1 + 8X₁ + 4X₂ + 2X₃ + X₄ + } + + const MaxStep = 4 + const MaxStart = 4 + hashGens := make([]func() hash.Hash, 0, MaxStart*MaxStep) + + for step := 0; step < MaxStep; step++ { + for startState := 0; startState < MaxStart; startState++ { + if step == 0 && startState == 1 { // unlucky case where a bad proof would be accepted + continue + } + hashGens = append(hashGens, newMessageCounterGenerator(startState, step)) + } + } + + for _, poly := range polys { + for _, hashGen := range hashGens { + assert.NoError(t, testSumcheckSingleClaimMultilin(poly, hashGen), + "failed with poly %v and hashGen %v", poly, hashGen()) + } + } +} diff --git a/internal/gkr/small_rational/sumcheck_test_vector_gen.go b/internal/gkr/small_rational/sumcheck_test_vector_gen.go new file mode 100644 index 00000000..ac9956af --- /dev/null +++ b/internal/gkr/small_rational/sumcheck_test_vector_gen.go @@ -0,0 +1,209 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "encoding/json" + "fmt" + "hash" + "math/bits" + "os" + "path/filepath" + "runtime/pprof" + + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/consensys/gnark/internal/small_rational" + "github.com/consensys/gnark/internal/small_rational/polynomial" +) + +func runMultilin(testCaseInfo *sumcheckTestCaseInfo) error { + + var poly polynomial.MultiLin + if v, err := sliceToElementSlice(testCaseInfo.Values); err == nil { + poly = v + } else { + return err + } + + var ( + hsh hash.Hash + err error + ) + + if hsh, err = hashFromDescription(testCaseInfo.Hash); err != nil { + return err + } + + proof, err := sumcheckProve( + &singleMultilinClaim{poly}, fiatshamir.WithHash(hsh)) + if err != nil { + return err + } + testCaseInfo.Proof = sumcheckToPrintableProof(proof) + + // Verification + if v, _err := sliceToElementSlice(testCaseInfo.Values); _err == nil { + poly = v + } else { + return _err + } + var claimedSum small_rational.SmallRational + if _, err = claimedSum.SetInterface(testCaseInfo.ClaimedSum); err != nil { + return err + } + + if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, fiatshamir.WithHash(hsh)); err != nil { + return fmt.Errorf("proof rejected: %v", err) + } + + proof.partialSumPolys[0][0].Add(&proof.partialSumPolys[0][0], toElement(1)) + if err = sumcheckVerify(singleMultilinLazyClaim{g: poly, claimedSum: claimedSum}, proof, fiatshamir.WithHash(hsh)); err == nil { + return fmt.Errorf("bad proof accepted") + } + + pprof.StopCPUProfile() + //return f.Close() + + return nil +} + +func runSumcheck(testCaseInfo *sumcheckTestCaseInfo) error { + switch testCaseInfo.Type { + case "multilin": + return runMultilin(testCaseInfo) + default: + return fmt.Errorf("type \"%s\" unrecognized", testCaseInfo.Type) + } +} + +func GenerateSumcheckVectors() error { + // read the test vectors file, generate the proof, make sure it verifies, + // and add the proof to the same file + const relPath = "../../gkr/test_vectors/sumcheck/vectors.json" + + var filename string + var err error + if filename, err = filepath.Abs(relPath); err != nil { + return err + } + + var bytes []byte + + if bytes, err = os.ReadFile(filename); err != nil { + return err + } + + var testCasesInfo sumcheckTestCasesInfo + if err = json.Unmarshal(bytes, &testCasesInfo); err != nil { + return err + } + + failed := false + for name, testCase := range testCasesInfo { + if err = runSumcheck(testCase); err != nil { + fmt.Println(name, ":", err) + failed = true + } + } + + if failed { + return fmt.Errorf("test case failed") + } + + if bytes, err = json.MarshalIndent(testCasesInfo, "", "\t"); err != nil { + return err + } + + return os.WriteFile(filename, bytes, 0) +} + +type sumcheckTestCasesInfo map[string]*sumcheckTestCaseInfo + +type sumcheckTestCaseInfo struct { + Type string `json:"type"` + Hash gkrtesting.HashDescription `json:"hash"` + Values []interface{} `json:"values"` + Description string `json:"description"` + Proof SumcheckPrintableProof `json:"proof"` + ClaimedSum interface{} `json:"claimedSum"` +} + +type SumcheckPrintableProof struct { + PartialSumPolys [][]interface{} `json:"partialSumPolys"` + FinalEvalProof interface{} `json:"finalEvalProof"` +} + +func sumcheckToPrintableProof(proof sumcheckProof) (printable SumcheckPrintableProof) { + if proof.finalEvalProof != nil { + panic("null expected") + } + printable.FinalEvalProof = struct{}{} + printable.PartialSumPolys = elementSliceSliceToInterfaceSliceSlice(proof.partialSumPolys) + return +} + +type singleMultilinClaim struct { + g polynomial.MultiLin +} + +func (c singleMultilinClaim) proveFinalEval(r []small_rational.SmallRational) []small_rational.SmallRational { + return nil // verifier can compute the final eval itself +} + +func (c singleMultilinClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} + +func (c singleMultilinClaim) claimsNum() int { + return 1 +} + +func sumForX1One(g polynomial.MultiLin) polynomial.Polynomial { + sum := g[len(g)/2] + for i := len(g)/2 + 1; i < len(g); i++ { + sum.Add(&sum, &g[i]) + } + return []small_rational.SmallRational{sum} +} + +func (c singleMultilinClaim) combine(small_rational.SmallRational) polynomial.Polynomial { + return sumForX1One(c.g) +} + +func (c *singleMultilinClaim) next(r small_rational.SmallRational) polynomial.Polynomial { + c.g.Fold(r) + return sumForX1One(c.g) +} + +type singleMultilinLazyClaim struct { + g polynomial.MultiLin + claimedSum small_rational.SmallRational +} + +func (c singleMultilinLazyClaim) verifyFinalEval(r []small_rational.SmallRational, combinationCoeff small_rational.SmallRational, purportedValue small_rational.SmallRational, proof []small_rational.SmallRational) error { + val := c.g.Evaluate(r, nil) + if val.Equal(&purportedValue) { + return nil + } + return fmt.Errorf("mismatch") +} + +func (c singleMultilinLazyClaim) combinedSum(combinationCoeffs small_rational.SmallRational) small_rational.SmallRational { + return c.claimedSum +} + +func (c singleMultilinLazyClaim) degree(i int) int { + return 1 +} + +func (c singleMultilinLazyClaim) claimsNum() int { + return 1 +} + +func (c singleMultilinLazyClaim) varsNum() int { + return bits.TrailingZeros(uint(len(c.g))) +} diff --git a/internal/gkr/small_rational/test_vector_gen.go b/internal/gkr/small_rational/test_vector_gen.go new file mode 100644 index 00000000..18493cc3 --- /dev/null +++ b/internal/gkr/small_rational/test_vector_gen.go @@ -0,0 +1,283 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "encoding/json" + "fmt" + "hash" + "os" + "path/filepath" + "reflect" + + "github.com/consensys/bavard" + fiatshamir "github.com/consensys/gnark-crypto/fiat-shamir" + "github.com/consensys/gnark/internal/gkr/gkrtesting" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/internal/small_rational" + "github.com/consensys/gnark/internal/small_rational/polynomial" + "github.com/consensys/gnark/internal/utils" +) + +func GenerateVectors() error { + testDirPath, err := filepath.Abs("../../gkr/test_vectors") + if err != nil { + return err + } + + fmt.Printf("generating GKR test cases: scanning directory %s for test specs\n", testDirPath) + + dirEntries, err := os.ReadDir(testDirPath) + if err != nil { + return err + } + for _, dirEntry := range dirEntries { + if !dirEntry.IsDir() { + + if filepath.Ext(dirEntry.Name()) == ".json" { + path := filepath.Join(testDirPath, dirEntry.Name()) + if !bavard.ShouldGenerate(path) { + continue + } + fmt.Println("\tprocessing", dirEntry.Name()) + if err = run(path); err != nil { + return err + } + } + } + } + + return nil +} + +func run(absPath string) error { + testCase, err := newTestCase(absPath) + if err != nil { + return err + } + + transcriptSetting := fiatshamir.WithHash(testCase.Hash) + + var proof Proof + proof, err = Prove(testCase.Circuit, testCase.FullAssignment, transcriptSetting) + if err != nil { + return err + } + + if testCase.Info.Proof, err = toPrintableProof(proof); err != nil { + return err + } + var outBytes []byte + if outBytes, err = json.MarshalIndent(testCase.Info, "", "\t"); err == nil { + if err = os.WriteFile(absPath, outBytes, 0); err != nil { + return err + } + } else { + return err + } + + testCase, err = newTestCase(absPath) + if err != nil { + return err + } + + err = Verify(testCase.Circuit, testCase.InOutAssignment, proof, transcriptSetting) + if err != nil { + return err + } + + testCase, err = newTestCase(absPath) + if err != nil { + return err + } + + err = Verify(testCase.Circuit, testCase.InOutAssignment, proof, fiatshamir.WithHash(newMessageCounter(2, 0))) + if err == nil { + return fmt.Errorf("bad proof accepted") + } + return nil +} + +func toPrintableProof(proof Proof) (gkrtesting.PrintableProof, error) { + res := make(gkrtesting.PrintableProof, len(proof)) + + for i := range proof { + + partialSumPolys := make([][]interface{}, len(proof[i].partialSumPolys)) + for k, partialK := range proof[i].partialSumPolys { + partialSumPolys[k] = elementSliceToInterfaceSlice(partialK) + } + + res[i] = gkrtesting.PrintableSumcheckProof{ + FinalEvalProof: elementSliceToInterfaceSlice(proof[i].finalEvalProof), + PartialSumPolys: partialSumPolys, + } + } + return res, nil +} + +func elementToInterface(x *small_rational.SmallRational) interface{} { + if i := x.BigInt(nil); i != nil { + return i + } + return x.Text(10) +} + +func elementSliceToInterfaceSlice(x interface{}) []interface{} { + if x == nil { + return nil + } + + X := reflect.ValueOf(x) + + res := make([]interface{}, X.Len()) + for i := range res { + xI := X.Index(i).Interface().(small_rational.SmallRational) + res[i] = elementToInterface(&xI) + } + return res +} + +func elementSliceSliceToInterfaceSliceSlice(x interface{}) [][]interface{} { + if x == nil { + return nil + } + + X := reflect.ValueOf(x) + + res := make([][]interface{}, X.Len()) + for i := range res { + res[i] = elementSliceToInterfaceSlice(X.Index(i).Interface()) + } + + return res +} + +func unmarshalProof(printable gkrtesting.PrintableProof) (Proof, error) { + proof := make(Proof, len(printable)) + for i := range printable { + finalEvalProof := []small_rational.SmallRational(nil) + + if printable[i].FinalEvalProof != nil { + finalEvalSlice := reflect.ValueOf(printable[i].FinalEvalProof) + finalEvalProof = make([]small_rational.SmallRational, finalEvalSlice.Len()) + for k := range finalEvalProof { + if _, err := finalEvalProof[k].SetInterface(finalEvalSlice.Index(k).Interface()); err != nil { + return nil, err + } + } + } + + proof[i] = sumcheckProof{ + partialSumPolys: make([]polynomial.Polynomial, len(printable[i].PartialSumPolys)), + finalEvalProof: finalEvalProof, + } + for k := range printable[i].PartialSumPolys { + var err error + if proof[i].partialSumPolys[k], err = sliceToElementSlice(printable[i].PartialSumPolys[k]); err != nil { + return nil, err + } + } + } + return proof, nil +} + +type TestCase struct { + Circuit gkrtypes.Circuit + Hash hash.Hash + Proof Proof + FullAssignment WireAssignment + InOutAssignment WireAssignment + Info gkrtesting.TestCaseInfo // we are generating the test vectors, so we need to keep the circuit instance info to ADD the proof to it and resave it +} + +var ( + testCases = make(map[string]*TestCase) + cache = gkrtesting.NewCache() +) + +func newTestCase(path string) (*TestCase, error) { + path, err := filepath.Abs(path) + if err != nil { + return nil, err + } + dir := filepath.Dir(path) + + tCase, ok := testCases[path] + if ok { + return tCase, nil + } + + info, err := cache.ReadTestCaseInfo(path) + if err != nil { + return nil, err + } + + circuit := cache.GetCircuit(filepath.Join(dir, info.Circuit)) + var _hash hash.Hash + if _hash, err = hashFromDescription(info.Hash); err != nil { + return nil, err + } + var proof Proof + if proof, err = unmarshalProof(info.Proof); err != nil { + return nil, err + } + + fullAssignment := make(WireAssignment, len(circuit)) + inOutAssignment := make(WireAssignment, len(circuit)) + + sorted := circuit.TopologicalSort() + + inI, outI := 0, 0 + for i, w := range sorted { + var assignmentRaw []interface{} + if w.IsInput() { + if inI == len(info.Input) { + return nil, fmt.Errorf("fewer input in vector than in circuit") + } + assignmentRaw = info.Input[inI] + inI++ + } else if w.IsOutput() { + if outI == len(info.Output) { + return nil, fmt.Errorf("fewer output in vector than in circuit") + } + assignmentRaw = info.Output[outI] + outI++ + } + if assignmentRaw != nil { + var wireAssignment []small_rational.SmallRational + if wireAssignment, err = sliceToElementSlice(assignmentRaw); err != nil { + return nil, err + } + + fullAssignment[i] = wireAssignment + inOutAssignment[i] = wireAssignment + } + } + + fullAssignment.Complete(utils.References(circuit)) + + for i, w := range sorted { + if w.IsOutput() { + if err = sliceEquals(inOutAssignment[i], fullAssignment[i]); err != nil { + return nil, fmt.Errorf("assignment mismatch: %v", err) + } + } + } + + tCase = &TestCase{ + FullAssignment: fullAssignment, + InOutAssignment: inOutAssignment, + Proof: proof, + Hash: _hash, + Circuit: circuit, + Info: info, + } + + testCases[path] = tCase + + return tCase, nil +} diff --git a/internal/gkr/small_rational/test_vector_utils.go b/internal/gkr/small_rational/test_vector_utils.go new file mode 100644 index 00000000..89b2cf0e --- /dev/null +++ b/internal/gkr/small_rational/test_vector_utils.go @@ -0,0 +1,113 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by gnark DO NOT EDIT + +package gkr + +import ( + "fmt" + "hash" + + "github.com/consensys/gnark/internal/small_rational" + "github.com/consensys/gnark/internal/small_rational/polynomial" + + "github.com/consensys/gnark/internal/gkr/gkrtesting" +) + +func toElement(i int64) *small_rational.SmallRational { + var res small_rational.SmallRational + res.SetInt64(i) + return &res +} + +func hashFromDescription(d gkrtesting.HashDescription) (hash.Hash, error) { + if _type, ok := d["type"]; ok { + switch _type { + case "const": + startState := int64(d["val"].(float64)) + return &messageCounter{startState: startState, step: 0, state: startState}, nil + default: + return nil, fmt.Errorf("unknown fake hash type \"%s\"", _type) + } + } + return nil, fmt.Errorf("hash description missing type") +} + +type messageCounter struct { + startState int64 + state int64 + step int64 +} + +func (m *messageCounter) Write(p []byte) (n int, err error) { + inputBlockSize := (len(p)-1)/small_rational.Bytes + 1 + m.state += int64(inputBlockSize) * m.step + return len(p), nil +} + +func (m *messageCounter) Sum(b []byte) []byte { + inputBlockSize := (len(b)-1)/small_rational.Bytes + 1 + resI := m.state + int64(inputBlockSize)*m.step + var res small_rational.SmallRational + res.SetInt64(int64(resI)) + resBytes := res.Bytes() + return resBytes[:] +} + +func (m *messageCounter) Reset() { + m.state = m.startState +} + +func (m *messageCounter) Size() int { + return small_rational.Bytes +} + +func (m *messageCounter) BlockSize() int { + return small_rational.Bytes +} + +func newMessageCounter(startState, step int) hash.Hash { + transcript := &messageCounter{startState: int64(startState), state: int64(startState), step: int64(step)} + return transcript +} + +func newMessageCounterGenerator(startState, step int) func() hash.Hash { + return func() hash.Hash { + return newMessageCounter(startState, step) + } +} + +func sliceToElementSlice[T any](slice []T) ([]small_rational.SmallRational, error) { + elementSlice := make([]small_rational.SmallRational, len(slice)) + for i, v := range slice { + if _, err := elementSlice[i].SetInterface(v); err != nil { + return nil, err + } + } + return elementSlice, nil +} + +func sliceEquals(a []small_rational.SmallRational, b []small_rational.SmallRational) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if !a[i].Equal(&b[i]) { + return fmt.Errorf("at index %d: %s ≠ %s", i, a[i].String(), b[i].String()) + } + } + return nil +} + +func polynomialSliceEquals(a []polynomial.Polynomial, b []polynomial.Polynomial) error { + if len(a) != len(b) { + return fmt.Errorf("length mismatch %d≠%d", len(a), len(b)) + } + for i := range a { + if err := sliceEquals(a[i], b[i]); err != nil { + return fmt.Errorf("at index %d: %w", i, err) + } + } + return nil +} diff --git a/std/sumcheck/sumcheck.go b/internal/gkr/sumcheck.go similarity index 57% rename from std/sumcheck/sumcheck.go rename to internal/gkr/sumcheck.go index ad96621c..77e327cc 100644 --- a/std/sumcheck/sumcheck.go +++ b/internal/gkr/sumcheck.go @@ -1,4 +1,4 @@ -package sumcheck +package gkr import ( "errors" @@ -9,19 +9,21 @@ import ( "github.com/consensys/gnark/std/polynomial" ) -// LazyClaims is the Claims data structure on the verifier side. It is "lazy" in that it has to compute fewer things. -type LazyClaims interface { - ClaimsNum() int // ClaimsNum = m - VarsNum() int // VarsNum = n - CombinedSum(api frontend.API, a frontend.Variable) frontend.Variable // CombinedSum returns c = ∑_{1≤j≤m} aʲ⁻¹cⱼ - Degree(i int) int //Degree of the total claim in the i'th variable - VerifyFinalEval(api frontend.API, r []frontend.Variable, combinationCoeff, purportedValue frontend.Variable, proof interface{}) error +// A SNARK gadget capable of verifying sumcheck proofs + +// sumcheckLazyClaims is the Claims data structure on the verifier side. It is "lazy" in that it has to compute fewer things. +type sumcheckLazyClaims interface { + claimsNum() int // claimsNum = m + varsNum() int // varsNum = n + combinedSum(api frontend.API, a frontend.Variable) frontend.Variable // combinedSum returns c = ∑_{1≤j≤m} aʲ⁻¹cⱼ + degree(i int) int // degree of the total claim in the i'th variable + verifyFinalEval(api frontend.API, r []frontend.Variable, combinationCoeff, purportedValue frontend.Variable, proof []frontend.Variable) error } -// Proof of a multi-sumcheck statement. -type Proof struct { +// sumcheckProof of a multi-sumcheck statement. +type sumcheckProof struct { PartialSumPolys []polynomial.Polynomial - FinalEvalProof interface{} + FinalEvalProof []frontend.Variable } func setupTranscript(api frontend.API, claimsNum int, varsNum int, settings *fiatshamir.Settings) ([]string, error) { @@ -55,9 +57,9 @@ func next(transcript *fiatshamir.Transcript, bindings []frontend.Variable, remai return res, err } -func Verify(api frontend.API, claims LazyClaims, proof Proof, transcriptSettings fiatshamir.Settings) error { +func verifySumcheck(api frontend.API, claims sumcheckLazyClaims, proof sumcheckProof, transcriptSettings fiatshamir.Settings) error { - remainingChallengeNames, err := setupTranscript(api, claims.ClaimsNum(), claims.VarsNum(), &transcriptSettings) + remainingChallengeNames, err := setupTranscript(api, claims.claimsNum(), claims.varsNum(), &transcriptSettings) transcript := transcriptSettings.Transcript if err != nil { return err @@ -65,28 +67,28 @@ func Verify(api frontend.API, claims LazyClaims, proof Proof, transcriptSettings var combinationCoeff frontend.Variable - if claims.ClaimsNum() >= 2 { + if claims.claimsNum() >= 2 { if combinationCoeff, err = next(transcript, []frontend.Variable{}, &remainingChallengeNames); err != nil { return err } } - r := make([]frontend.Variable, claims.VarsNum()) + r := make([]frontend.Variable, claims.varsNum()) // Just so that there is enough room for gJ to be reused - maxDegree := claims.Degree(0) - for j := 1; j < claims.VarsNum(); j++ { - if d := claims.Degree(j); d > maxDegree { + maxDegree := claims.degree(0) + for j := 1; j < claims.varsNum(); j++ { + if d := claims.degree(j); d > maxDegree { maxDegree = d } } - gJ := make(polynomial.Polynomial, maxDegree+1) //At the end of iteration j, gJ = ∑_{i < 2ⁿ⁻ʲ⁻¹} g(X₁, ..., Xⱼ₊₁, i...) NOTE: n is shorthand for claims.VarsNum() - gJR := claims.CombinedSum(api, combinationCoeff) // At the beginning of iteration j, gJR = ∑_{i < 2ⁿ⁻ʲ} g(r₁, ..., rⱼ, i...) + gJ := make(polynomial.Polynomial, maxDegree+1) //At the end of iteration j, gJ = ∑_{i < 2ⁿ⁻ʲ⁻¹} g(X₁, ..., Xⱼ₊₁, i...) NOTE: n is shorthand for claims.varsNum() + gJR := claims.combinedSum(api, combinationCoeff) // At the beginning of iteration j, gJR = ∑_{i < 2ⁿ⁻ʲ} g(r₁, ..., rⱼ, i...) - for j := 0; j < claims.VarsNum(); j++ { + for j := 0; j < claims.varsNum(); j++ { partialSumPoly := proof.PartialSumPolys[j] //proof.PartialSumPolys(j) - if len(partialSumPoly) != claims.Degree(j) { + if len(partialSumPoly) != claims.degree(j) { return errors.New("malformed proof") //Malformed proof } copy(gJ[1:], partialSumPoly) @@ -98,9 +100,9 @@ func Verify(api frontend.API, claims LazyClaims, proof Proof, transcriptSettings return err } - gJR = polynomial.InterpolateLDE(api, r[j], gJ[:(claims.Degree(j)+1)]) + gJR = polynomial.InterpolateLDE(api, r[j], gJ[:(claims.degree(j)+1)]) } - return claims.VerifyFinalEval(api, r, combinationCoeff, gJR, proof.FinalEvalProof) + return claims.verifyFinalEval(api, r, combinationCoeff, gJR, proof.FinalEvalProof) } diff --git a/std/gkr/test_vectors/resources/mimc_five_levels.json b/internal/gkr/test_vectors/circuits/mimc_five_levels.json similarity index 100% rename from std/gkr/test_vectors/resources/mimc_five_levels.json rename to internal/gkr/test_vectors/circuits/mimc_five_levels.json diff --git a/std/gkr/test_vectors/resources/single_identity_gate.json b/internal/gkr/test_vectors/circuits/single_identity_gate.json similarity index 100% rename from std/gkr/test_vectors/resources/single_identity_gate.json rename to internal/gkr/test_vectors/circuits/single_identity_gate.json diff --git a/std/gkr/test_vectors/resources/single_input_two_identity_gates.json b/internal/gkr/test_vectors/circuits/single_input_two_identity_gates.json similarity index 100% rename from std/gkr/test_vectors/resources/single_input_two_identity_gates.json rename to internal/gkr/test_vectors/circuits/single_input_two_identity_gates.json diff --git a/std/gkr/test_vectors/resources/single_input_two_outs.json b/internal/gkr/test_vectors/circuits/single_input_two_outs.json similarity index 86% rename from std/gkr/test_vectors/resources/single_input_two_outs.json rename to internal/gkr/test_vectors/circuits/single_input_two_outs.json index c577c1ca..3a39e562 100644 --- a/std/gkr/test_vectors/resources/single_input_two_outs.json +++ b/internal/gkr/test_vectors/circuits/single_input_two_outs.json @@ -4,7 +4,7 @@ "inputs": [] }, { - "gate": "mul", + "gate": "mul2", "inputs": [0, 0] }, { diff --git a/std/gkr/test_vectors/resources/single_mimc_gate.json b/internal/gkr/test_vectors/circuits/single_mimc_gate.json similarity index 100% rename from std/gkr/test_vectors/resources/single_mimc_gate.json rename to internal/gkr/test_vectors/circuits/single_mimc_gate.json diff --git a/std/gkr/test_vectors/resources/single_mul_gate.json b/internal/gkr/test_vectors/circuits/single_mul_gate.json similarity index 85% rename from std/gkr/test_vectors/resources/single_mul_gate.json rename to internal/gkr/test_vectors/circuits/single_mul_gate.json index 0f65a07e..d009ebe0 100644 --- a/std/gkr/test_vectors/resources/single_mul_gate.json +++ b/internal/gkr/test_vectors/circuits/single_mul_gate.json @@ -8,7 +8,7 @@ "inputs": [] }, { - "gate": "mul", + "gate": "mul2", "inputs": [0, 1] } ] \ No newline at end of file diff --git a/std/gkr/test_vectors/resources/two_identity_gates_composed_single_input.json b/internal/gkr/test_vectors/circuits/two_identity_gates_composed_single_input.json similarity index 100% rename from std/gkr/test_vectors/resources/two_identity_gates_composed_single_input.json rename to internal/gkr/test_vectors/circuits/two_identity_gates_composed_single_input.json diff --git a/std/gkr/test_vectors/resources/two_inputs_select-input-3_gate.json b/internal/gkr/test_vectors/circuits/two_inputs_select-input-3_gate.json similarity index 100% rename from std/gkr/test_vectors/resources/two_inputs_select-input-3_gate.json rename to internal/gkr/test_vectors/circuits/two_inputs_select-input-3_gate.json diff --git a/internal/gkr/test_vectors/generate.go b/internal/gkr/test_vectors/generate.go new file mode 100644 index 00000000..b78f4c92 --- /dev/null +++ b/internal/gkr/test_vectors/generate.go @@ -0,0 +1,14 @@ +package main + +import gkr "github.com/consensys/gnark/internal/gkr/small_rational" + +func main() { + assertNoError(gkr.GenerateSumcheckVectors()) + assertNoError(gkr.GenerateVectors()) +} + +func assertNoError(err error) { + if err != nil { + panic(err) + } +} diff --git a/std/gkr/test_vectors/mimc_five_levels_two_instances._json b/internal/gkr/test_vectors/mimc_five_levels_two_instances._json similarity index 83% rename from std/gkr/test_vectors/mimc_five_levels_two_instances._json rename to internal/gkr/test_vectors/mimc_five_levels_two_instances._json index 446d23fd..e980cfb0 100644 --- a/std/gkr/test_vectors/mimc_five_levels_two_instances._json +++ b/internal/gkr/test_vectors/mimc_five_levels_two_instances._json @@ -1,6 +1,6 @@ { "hash": {"type": "const", "val": -1}, - "circuit": "resources/mimc_five_levels.json", + "circuit": "circuits/mimc_five_levels.json", "input": [[1, 3], [1, 3], [1, 3], [1, 3], [1, 3], [1, 3]], "output": [[4, 3]], "proof": [[{"partialSumPolys":[[3,4]],"finalEvalProof":[3]}],[{"partialSumPolys":null,"finalEvalProof":null}]] diff --git a/std/gkr/test_vectors/single_identity_gate_two_instances.json b/internal/gkr/test_vectors/single_identity_gate_two_instances.json similarity index 85% rename from std/gkr/test_vectors/single_identity_gate_two_instances.json rename to internal/gkr/test_vectors/single_identity_gate_two_instances.json index ce326d0a..ba28e359 100644 --- a/std/gkr/test_vectors/single_identity_gate_two_instances.json +++ b/internal/gkr/test_vectors/single_identity_gate_two_instances.json @@ -3,7 +3,7 @@ "type": "const", "val": -1 }, - "circuit": "resources/single_identity_gate.json", + "circuit": "circuits/single_identity_gate.json", "input": [ [ 4, diff --git a/std/gkr/test_vectors/single_input_two_identity_gates_two_instances.json b/internal/gkr/test_vectors/single_input_two_identity_gates_two_instances.json similarity index 87% rename from std/gkr/test_vectors/single_input_two_identity_gates_two_instances.json rename to internal/gkr/test_vectors/single_input_two_identity_gates_two_instances.json index 2c95f044..1451b332 100644 --- a/std/gkr/test_vectors/single_input_two_identity_gates_two_instances.json +++ b/internal/gkr/test_vectors/single_input_two_identity_gates_two_instances.json @@ -3,7 +3,7 @@ "type": "const", "val": -1 }, - "circuit": "resources/single_input_two_identity_gates.json", + "circuit": "circuits/single_input_two_identity_gates.json", "input": [ [ 2, diff --git a/std/gkr/test_vectors/single_input_two_outs_two_instances.json b/internal/gkr/test_vectors/single_input_two_outs_two_instances.json similarity index 89% rename from std/gkr/test_vectors/single_input_two_outs_two_instances.json rename to internal/gkr/test_vectors/single_input_two_outs_two_instances.json index d348303d..897aea7e 100644 --- a/std/gkr/test_vectors/single_input_two_outs_two_instances.json +++ b/internal/gkr/test_vectors/single_input_two_outs_two_instances.json @@ -3,7 +3,7 @@ "type": "const", "val": -1 }, - "circuit": "resources/single_input_two_outs.json", + "circuit": "circuits/single_input_two_outs.json", "input": [ [ 1, diff --git a/std/gkr/test_vectors/single_mimc_gate_four_instances.json b/internal/gkr/test_vectors/single_mimc_gate_four_instances.json similarity index 64% rename from std/gkr/test_vectors/single_mimc_gate_four_instances.json rename to internal/gkr/test_vectors/single_mimc_gate_four_instances.json index 525459ec..a724ba5a 100644 --- a/std/gkr/test_vectors/single_mimc_gate_four_instances.json +++ b/internal/gkr/test_vectors/single_mimc_gate_four_instances.json @@ -3,7 +3,7 @@ "type": "const", "val": -1 }, - "circuit": "resources/single_mimc_gate.json", + "circuit": "circuits/single_mimc_gate.json", "input": [ [ 1, @@ -45,21 +45,21 @@ -32640, -2239484, -29360128, - "-200000010", - "-931628672", - "-3373267120", - "-10200858624", - "-26939400158" + -200000010, + -931628672, + -3373267120, + -10200858624, + -26939400158 ], [ -81920, -41943040, - "-1254113280", - "-13421772800", - "-83200000000", - "-366917713920", - "-1281828208640", - "-3779571220480" + -1254113280, + -13421772800, + -83200000000, + -366917713920, + -1281828208640, + -3779571220480 ] ] } diff --git a/std/gkr/test_vectors/single_mimc_gate_two_instances.json b/internal/gkr/test_vectors/single_mimc_gate_two_instances.json similarity index 87% rename from std/gkr/test_vectors/single_mimc_gate_two_instances.json rename to internal/gkr/test_vectors/single_mimc_gate_two_instances.json index 7fa23ce4..901db486 100644 --- a/std/gkr/test_vectors/single_mimc_gate_two_instances.json +++ b/internal/gkr/test_vectors/single_mimc_gate_two_instances.json @@ -3,7 +3,7 @@ "type": "const", "val": -1 }, - "circuit": "resources/single_mimc_gate.json", + "circuit": "circuits/single_mimc_gate.json", "input": [ [ 1, @@ -43,7 +43,7 @@ -10706059, -33554432, -90876411, - "-220000000" + -220000000 ] ] } diff --git a/std/gkr/test_vectors/single_mul_gate_two_instances.json b/internal/gkr/test_vectors/single_mul_gate_two_instances.json similarity index 89% rename from std/gkr/test_vectors/single_mul_gate_two_instances.json rename to internal/gkr/test_vectors/single_mul_gate_two_instances.json index 75c1d59c..b85a6df4 100644 --- a/std/gkr/test_vectors/single_mul_gate_two_instances.json +++ b/internal/gkr/test_vectors/single_mul_gate_two_instances.json @@ -3,7 +3,7 @@ "type": "const", "val": -1 }, - "circuit": "resources/single_mul_gate.json", + "circuit": "circuits/single_mul_gate.json", "input": [ [ 4, diff --git a/internal/gkr/test_vectors/sumcheck/vectors.json b/internal/gkr/test_vectors/sumcheck/vectors.json new file mode 100644 index 00000000..64b8e3fb --- /dev/null +++ b/internal/gkr/test_vectors/sumcheck/vectors.json @@ -0,0 +1,56 @@ +{ + "linear_univariate_single_claim": { + "type": "multilin", + "hash": { + "type": "const", + "val": -1 + }, + "values": [ + 1, + 3 + ], + "description": "X ↦ 2X + 1", + "proof": { + "partialSumPolys": [ + [ + 3 + ] + ], + "finalEvalProof": {} + }, + "claimedSum": 4 + }, + "trilinear_single_claim": { + "type": "multilin", + "hash": { + "type": "const", + "val": -1 + }, + "values": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + "description": "X₁, X₂, X₃ ↦ 1 + 4X₁ + 2X₂ + X₃", + "proof": { + "partialSumPolys": [ + [ + 26 + ], + [ + -1 + ], + [ + -4 + ] + ], + "finalEvalProof": {} + }, + "claimedSum": 36 + } +} \ No newline at end of file diff --git a/std/gkr/test_vectors/two_identity_gates_composed_single_input_two_instances.json b/internal/gkr/test_vectors/two_identity_gates_composed_single_input_two_instances.json similarity index 84% rename from std/gkr/test_vectors/two_identity_gates_composed_single_input_two_instances.json rename to internal/gkr/test_vectors/two_identity_gates_composed_single_input_two_instances.json index 10e5f1ff..69a2038a 100644 --- a/std/gkr/test_vectors/two_identity_gates_composed_single_input_two_instances.json +++ b/internal/gkr/test_vectors/two_identity_gates_composed_single_input_two_instances.json @@ -3,7 +3,7 @@ "type": "const", "val": -1 }, - "circuit": "resources/two_identity_gates_composed_single_input.json", + "circuit": "circuits/two_identity_gates_composed_single_input.json", "input": [ [ 2, diff --git a/std/gkr/test_vectors/two_inputs_select-input-3_gate_two_instances.json b/internal/gkr/test_vectors/two_inputs_select-input-3_gate_two_instances.json similarity index 86% rename from std/gkr/test_vectors/two_inputs_select-input-3_gate_two_instances.json rename to internal/gkr/test_vectors/two_inputs_select-input-3_gate_two_instances.json index 19e127df..2dca0746 100644 --- a/std/gkr/test_vectors/two_inputs_select-input-3_gate_two_instances.json +++ b/internal/gkr/test_vectors/two_inputs_select-input-3_gate_two_instances.json @@ -3,7 +3,7 @@ "type": "const", "val": -1 }, - "circuit": "resources/two_inputs_select-input-3_gate.json", + "circuit": "circuits/two_inputs_select-input-3_gate.json", "input": [ [ 0, diff --git a/internal/gkr/utils_test.go b/internal/gkr/utils_test.go new file mode 100644 index 00000000..d0c489a6 --- /dev/null +++ b/internal/gkr/utils_test.go @@ -0,0 +1,344 @@ +package gkr + +import ( + "fmt" + "strconv" + "strings" + "testing" + + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/test" + "github.com/stretchr/testify/assert" +) + +// This file contains test vector utilities and unit tests related to them. + +// These data structures fail to equate different representations of the same number. i.e. 5 = -10/-2 +// @Tabaie TODO Replace with proper lookup tables + +type varsMap struct { + keys []frontend.Variable + values []frontend.Variable +} + +func getDelta(api frontend.API, x frontend.Variable, deltaIndex int, keys []frontend.Variable) frontend.Variable { + num := frontend.Variable(1) + den := frontend.Variable(1) + + for i, key := range keys { + if i != deltaIndex { + num = api.Mul(num, api.Sub(key, x)) + den = api.Mul(den, api.Sub(key, keys[deltaIndex])) + } + } + + return api.Div(num, den) +} + +// get returns garbage if key is not present +func (m varsMap) get(api frontend.API, key frontend.Variable) frontend.Variable { + res := frontend.Variable(0) + + for i := range m.keys { + deltaI := getDelta(api, key, i, m.keys) + res = api.MulAcc(res, deltaI, m.values[i]) + } + + return res +} + +// The keys in a doubleMap must be constant. i.e. known at setup time +type doubleMap struct { + keys1 []frontend.Variable + keys2 []frontend.Variable + values [][]frontend.Variable +} + +// get is very inefficient. Do not use outside testing +func (m doubleMap) get(api frontend.API, key1, key2 frontend.Variable) frontend.Variable { + deltas1 := make([]frontend.Variable, len(m.keys1)) + deltas2 := make([]frontend.Variable, len(m.keys2)) + + for i := range deltas1 { + deltas1[i] = getDelta(api, key1, i, m.keys1) + } + + for j := range deltas2 { + deltas2[j] = getDelta(api, key2, j, m.keys2) + } + + res := frontend.Variable(0) + + for i := range deltas1 { + for j := range deltas2 { + if m.values[i][j] != nil { + deltaIJ := api.Mul(deltas1[i], deltas2[j], m.values[i][j]) + res = api.Add(res, deltaIJ) + } + } + } + + return res +} + +func register[K comparable](m map[K]int, key K) { + if _, ok := m[key]; !ok { + m[key] = len(m) + } +} + +func orderKeys[K comparable](order map[K]int) (ordered []K) { + ordered = make([]K, len(order)) + for k, i := range order { + ordered[i] = k + } + return +} + +type elementMap struct { + single varsMap + double doubleMap +} + +func readMap(in map[string]interface{}) elementMap { + single := varsMap{ + keys: make([]frontend.Variable, 0), + values: make([]frontend.Variable, 0), + } + + keys1 := make(map[string]int) + keys2 := make(map[string]int) + + for k, v := range in { + + kSep := strings.Split(k, ",") + switch len(kSep) { + case 1: + single.keys = append(single.keys, k) + single.values = append(single.values, toVariable(v)) + case 2: + + register(keys1, kSep[0]) + register(keys2, kSep[1]) + + default: + panic("too many keys") + } + } + + vals := make([][]frontend.Variable, len(keys1)) + for i := range vals { + vals[i] = make([]frontend.Variable, len(keys2)) + } + + for k, v := range in { + kSep := strings.Split(k, ",") + if len(kSep) == 2 { + i1 := keys1[kSep[0]] + i2 := keys2[kSep[1]] + vals[i1][i2] = toVariable(v) + } + } + + double := doubleMap{ + keys1: toVariableSlice(orderKeys(keys1)), + keys2: toVariableSlice(orderKeys(keys2)), + values: vals, + } + + return elementMap{ + single: single, + double: double, + } +} + +func toVariable(v interface{}) frontend.Variable { + switch vT := v.(type) { + case float64: + return int(vT) + default: + return v + } +} + +func toVariableSlice[V any](slice []V) (variableSlice []frontend.Variable) { + variableSlice = make([]frontend.Variable, len(slice)) + for i := range slice { + variableSlice[i] = toVariable(slice[i]) + } + return +} + +func toVariableSliceSlice[V any](sliceSlice [][]V) (variableSliceSlice [][]frontend.Variable) { + variableSliceSlice = make([][]frontend.Variable, len(sliceSlice)) + for i := range sliceSlice { + variableSliceSlice[i] = toVariableSlice(sliceSlice[i]) + } + return +} + +func toMap(keys1, keys2, values []frontend.Variable) map[string]interface{} { + res := make(map[string]interface{}, len(keys1)) + for i := range keys1 { + str := strconv.Itoa(keys1[i].(int)) + "," + strconv.Itoa(keys2[i].(int)) + res[str] = values[i].(int) + } + return res +} + +func assertSliceEqual[T comparable](t *testing.T, expected, seen []T) { + assert.Equal(t, len(expected), len(seen)) + for i := range seen { + assert.True(t, expected[i] == seen[i], "@%d: %v != %v", i, expected[i], seen[i]) // assert.Equal is not strict enough when comparing pointers, i.e. it compares what they refer to + } +} + +func sliceEqual[T comparable](expected, seen []T) bool { + if len(expected) != len(seen) { + return false + } + for i := range seen { + if expected[i] != seen[i] { + return false + } + } + return true +} + +type testSingleMapCircuit struct { + M varsMap `gnark:"-"` + Values []frontend.Variable +} + +func (c *testSingleMapCircuit) Define(api frontend.API) error { + + for i, k := range c.M.keys { + v := c.M.get(api, k) + api.AssertIsEqual(v, c.Values[i]) + } + + return nil +} + +func TestSingleMap(t *testing.T) { + m := map[string]interface{}{ + "1": -2, + "4": 1, + "6": 7, + } + single := readMap(m).single + + assignment := testSingleMapCircuit{ + M: single, + Values: single.values, + } + + circuit := testSingleMapCircuit{ + M: single, + Values: make([]frontend.Variable, len(m)), // Okay to use the same object? + } + + test.NewAssert(t).CheckCircuit(&circuit, test.WithValidAssignment(&assignment)) +} + +type testDoubleMapCircuit struct { + M doubleMap `gnark:"-"` + Values []frontend.Variable + Keys1 []frontend.Variable `gnark:"-"` + Keys2 []frontend.Variable `gnark:"-"` +} + +func (c *testDoubleMapCircuit) Define(api frontend.API) error { + + for i := range c.Keys1 { + v := c.M.get(api, c.Keys1[i], c.Keys2[i]) + api.AssertIsEqual(v, c.Values[i]) + } + + return nil +} + +func TestReadDoubleMap(t *testing.T) { + keys1 := []frontend.Variable{1, 2} + keys2 := []frontend.Variable{1, 0} + values := []frontend.Variable{3, 1} + + for i := 0; i < 100; i++ { + m := toMap(keys1, keys2, values) + double := readMap(m).double + valuesOrdered := [][]frontend.Variable{{3, nil}, {nil, 1}} + + assert.True(t, double.keys1[0] == "1" && double.keys1[1] == "2" || double.keys1[0] == "2" && double.keys1[1] == "1") + assert.True(t, double.keys2[0] == "1" && double.keys2[1] == "0" || double.keys2[0] == "0" && double.keys2[1] == "1") + + if double.keys1[0] != "1" { + valuesOrdered[0], valuesOrdered[1] = valuesOrdered[1], valuesOrdered[0] + } + + if double.keys2[0] != "1" { + valuesOrdered[0][0], valuesOrdered[0][1] = valuesOrdered[0][1], valuesOrdered[0][0] + valuesOrdered[1][0], valuesOrdered[1][1] = valuesOrdered[1][1], valuesOrdered[1][0] + } + + assert.True(t, slice2Eq(valuesOrdered, double.values)) + + } + +} + +func slice2Eq(s1, s2 [][]frontend.Variable) bool { + if len(s1) != len(s2) { + return false + } + for i := range s1 { + if !sliceEq(s1[i], s2[i]) { + return false + } + } + return true +} + +func sliceEq(s1, s2 []frontend.Variable) bool { + if len(s1) != len(s2) { + return false + } + for i := range s1 { + if s1[i] != s2[i] { + return false + } + } + return true +} + +func TestDoubleMap(t *testing.T) { + keys1 := []frontend.Variable{1, 5, 5, 3} + keys2 := []frontend.Variable{1, -5, 4, 4} + values := []frontend.Variable{0, 2, 3, 0} + + m := toMap(keys1, keys2, values) + double := readMap(m).double + + fmt.Println(double) + + assignment := testDoubleMapCircuit{ + M: double, + Values: values, + Keys1: keys1, + Keys2: keys2, + } + + circuit := testDoubleMapCircuit{ + M: double, + Keys1: keys1, + Keys2: keys2, + Values: make([]frontend.Variable, len(m)), // Okay to use the same object? + } + + test.NewAssert(t).CheckCircuit(&circuit, test.WithValidAssignment(&assignment)) +} + +func TestDoubleMapManyTimes(t *testing.T) { + for i := 0; i < 100; i++ { + TestDoubleMap(t) + } +} diff --git a/internal/regression_tests/issue1045/issue_1045_test.go b/internal/regression_tests/issue1045/issue_1045_test.go index a6a04e78..4f2a2cc5 100644 --- a/internal/regression_tests/issue1045/issue_1045_test.go +++ b/internal/regression_tests/issue1045/issue_1045_test.go @@ -10,6 +10,7 @@ import ( "github.com/consensys/gnark/backend/groth16" "github.com/consensys/gnark/backend/plonk" "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" @@ -65,7 +66,7 @@ func TestCircuitCompile(t *testing.T) { for _, bb := range []struct { builder frontend.NewBuilder tag string - }{{scs.NewBuilder, "scs"}, {r1cs.NewBuilder, "r1cs"}} { + }{{scs.NewBuilder[constraint.U64], "scs"}, {r1cs.NewBuilder[constraint.U64], "r1cs"}} { ccs, err := frontend.Compile(ecc.BN254.ScalarField(), bb.builder, &Circuit{}) assert.NoError(err) f, err := os.Create("testdata/issue1045." + bb.tag) diff --git a/internal/regression_tests/issue1045/testdata/issue1045.r1cs b/internal/regression_tests/issue1045/testdata/issue1045.r1cs index e9d2ca03..cffb20ba 100644 Binary files a/internal/regression_tests/issue1045/testdata/issue1045.r1cs and b/internal/regression_tests/issue1045/testdata/issue1045.r1cs differ diff --git a/internal/regression_tests/issue1045/testdata/issue1045.scs b/internal/regression_tests/issue1045/testdata/issue1045.scs index f2440767..2ba24e85 100644 Binary files a/internal/regression_tests/issue1045/testdata/issue1045.scs and b/internal/regression_tests/issue1045/testdata/issue1045.scs differ diff --git a/internal/small_rational/polynomial/doc.go b/internal/small_rational/polynomial/doc.go new file mode 100644 index 00000000..95ba2f13 --- /dev/null +++ b/internal/small_rational/polynomial/doc.go @@ -0,0 +1,5 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Package polynomial provides polynomial methods and commitment schemes. +package polynomial diff --git a/internal/small_rational/polynomial/multilin.go b/internal/small_rational/polynomial/multilin.go new file mode 100644 index 00000000..a857aa0a --- /dev/null +++ b/internal/small_rational/polynomial/multilin.go @@ -0,0 +1,177 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package polynomial + +import ( + "math/bits" + + "github.com/consensys/gnark-crypto/utils" + "github.com/consensys/gnark/internal/small_rational" +) + +// MultiLin tracks the values of a (dense i.e. not sparse) multilinear polynomial +// The variables are X₁ through Xₙ where n = log(len(.)) +// .[∑ᵢ 2ⁱ⁻¹ bₙ₋ᵢ] = the polynomial evaluated at (b₁, b₂, ..., bₙ) +// It is understood that any hypercube evaluation can be extrapolated to a multilinear polynomial +type MultiLin []small_rational.SmallRational + +// Fold is partial evaluation function k[X₁, X₂, ..., Xₙ] → k[X₂, ..., Xₙ] by setting X₁=r +func (m *MultiLin) Fold(r small_rational.SmallRational) { + mid := len(*m) / 2 + + bottom, top := (*m)[:mid], (*m)[mid:] + + var t small_rational.SmallRational // no need to update the top part + + // updating bookkeeping table + // knowing that the polynomial f ∈ (k[X₂, ..., Xₙ])[X₁] is linear, we would get f(r) = f(0) + r(f(1) - f(0)) + // the following loop computes the evaluations of f(r) accordingly: + // f(r, b₂, ..., bₙ) = f(0, b₂, ..., bₙ) + r(f(1, b₂, ..., bₙ) - f(0, b₂, ..., bₙ)) + for i := 0; i < mid; i++ { + // table[i] ← table[i] + r (table[i + mid] - table[i]) + t.Sub(&top[i], &bottom[i]) + t.Mul(&t, &r) + bottom[i].Add(&bottom[i], &t) + } + + *m = (*m)[:mid] +} + +func (m *MultiLin) FoldParallel(r small_rational.SmallRational) utils.Task { + mid := len(*m) / 2 + bottom, top := (*m)[:mid], (*m)[mid:] + + *m = bottom + + return func(start, end int) { + var t small_rational.SmallRational // no need to update the top part + for i := start; i < end; i++ { + // table[i] ← table[i] + r (table[i + mid] - table[i]) + t.Sub(&top[i], &bottom[i]) + t.Mul(&t, &r) + bottom[i].Add(&bottom[i], &t) + } + } +} + +func (m MultiLin) Sum() small_rational.SmallRational { + s := m[0] + for i := 1; i < len(m); i++ { + s.Add(&s, &m[i]) + } + return s +} + +func _clone(m MultiLin, p *Pool) MultiLin { + if p == nil { + return m.Clone() + } else { + return p.Clone(m) + } +} + +func _dump(m MultiLin, p *Pool) { + if p != nil { + p.Dump(m) + } +} + +// Evaluate extrapolate the value of the multilinear polynomial corresponding to m +// on the given coordinates +func (m MultiLin) Evaluate(coordinates []small_rational.SmallRational, p *Pool) small_rational.SmallRational { + // Folding is a mutating operation + bkCopy := _clone(m, p) + + // Evaluate step by step through repeated folding (i.e. evaluation at the first remaining variable) + for _, r := range coordinates { + bkCopy.Fold(r) + } + + result := bkCopy[0] + + _dump(bkCopy, p) + return result +} + +// Clone creates a deep copy of a bookkeeping table. +// Both multilinear interpolation and sumcheck require folding an underlying +// array, but folding changes the array. To do both one requires a deep copy +// of the bookkeeping table. +func (m MultiLin) Clone() MultiLin { + res := make(MultiLin, len(m)) + copy(res, m) + return res +} + +// Add two bookKeepingTables +func (m *MultiLin) Add(left, right MultiLin) { + size := len(left) + // Check that left and right have the same size + if len(right) != size || len(*m) != size { + panic("left, right and destination must have the right size") + } + + // Add elementwise + for i := 0; i < size; i++ { + (*m)[i].Add(&left[i], &right[i]) + } +} + +// EvalEq computes Eq(q₁, ... , qₙ, h₁, ... , hₙ) = Π₁ⁿ Eq(qᵢ, hᵢ) +// where Eq(x,y) = xy + (1-x)(1-y) = 1 - x - y + xy + xy interpolates +// +// _________________ +// | | | +// | 0 | 1 | +// |_______|_______| +// y | | | +// | 1 | 0 | +// |_______|_______| +// +// x +// +// In other words the polynomial evaluated here is the multilinear extrapolation of +// one that evaluates to q' == h' for vectors q', h' of binary values +func EvalEq(q, h []small_rational.SmallRational) small_rational.SmallRational { + var res, nxt, one, sum small_rational.SmallRational + one.SetOne() + for i := 0; i < len(q); i++ { + nxt.Mul(&q[i], &h[i]) // nxt <- qᵢ * hᵢ + nxt.Double(&nxt) // nxt <- 2 * qᵢ * hᵢ + nxt.Add(&nxt, &one) // nxt <- 1 + 2 * qᵢ * hᵢ + sum.Add(&q[i], &h[i]) // sum <- qᵢ + hᵢ TODO: Why not subtract one by one from nxt? More parallel? + + if i == 0 { + res.Sub(&nxt, &sum) // nxt <- 1 + 2 * qᵢ * hᵢ - qᵢ - hᵢ + } else { + nxt.Sub(&nxt, &sum) // nxt <- 1 + 2 * qᵢ * hᵢ - qᵢ - hᵢ + res.Mul(&res, &nxt) // res <- res * nxt + } + } + return res +} + +// Eq sets m to the representation of the polynomial Eq(q₁, ..., qₙ, *, ..., *) × m[0] +func (m *MultiLin) Eq(q []small_rational.SmallRational) { + n := len(q) + + if len(*m) != 1<= 0; i-- { + res.Mul(&res, v) + res.Add(&res, &(*p)[i]) + } + + return res +} + +// Clone returns a copy of the polynomial +func (p *Polynomial) Clone() Polynomial { + _p := make(Polynomial, len(*p)) + copy(_p, *p) + return _p +} + +// Set to another polynomial +func (p *Polynomial) Set(p1 Polynomial) { + if len(*p) != len(p1) { + *p = p1.Clone() + return + } + + for i := 0; i < len(p1); i++ { + (*p)[i].Set(&p1[i]) + } +} + +// AddConstantInPlace adds a constant to the polynomial, modifying p +func (p *Polynomial) AddConstantInPlace(c *small_rational.SmallRational) { + for i := 0; i < len(*p); i++ { + (*p)[i].Add(&(*p)[i], c) + } +} + +// SubConstantInPlace subs a constant to the polynomial, modifying p +func (p *Polynomial) SubConstantInPlace(c *small_rational.SmallRational) { + for i := 0; i < len(*p); i++ { + (*p)[i].Sub(&(*p)[i], c) + } +} + +// ScaleInPlace multiplies p by v, modifying p +func (p *Polynomial) ScaleInPlace(c *small_rational.SmallRational) { + for i := 0; i < len(*p); i++ { + (*p)[i].Mul(&(*p)[i], c) + } +} + +// Scale multiplies p0 by v, storing the result in p +func (p *Polynomial) Scale(c *small_rational.SmallRational, p0 Polynomial) { + if len(*p) != len(p0) { + *p = make(Polynomial, len(p0)) + } + for i := 0; i < len(p0); i++ { + (*p)[i].Mul(c, &p0[i]) + } +} + +// Add adds p1 to p2 +// This function allocates a new slice unless p == p1 or p == p2 +func (p *Polynomial) Add(p1, p2 Polynomial) *Polynomial { + + bigger := p1 + smaller := p2 + if len(bigger) < len(smaller) { + bigger, smaller = smaller, bigger + } + + if len(*p) == len(bigger) && (&(*p)[0] == &bigger[0]) { + for i := 0; i < len(smaller); i++ { + (*p)[i].Add(&(*p)[i], &smaller[i]) + } + return p + } + + if len(*p) == len(smaller) && (&(*p)[0] == &smaller[0]) { + for i := 0; i < len(smaller); i++ { + (*p)[i].Add(&(*p)[i], &bigger[i]) + } + *p = append(*p, bigger[len(smaller):]...) + return p + } + + res := make(Polynomial, len(bigger)) + copy(res, bigger) + for i := 0; i < len(smaller); i++ { + res[i].Add(&res[i], &smaller[i]) + } + *p = res + return p +} + +// Sub subtracts p2 from p1 +// TODO make interface more consistent with Add +func (p *Polynomial) Sub(p1, p2 Polynomial) *Polynomial { + if len(p1) != len(p2) || len(p2) != len(*p) { + return nil + } + for i := 0; i < len(*p); i++ { + (*p)[i].Sub(&p1[i], &p2[i]) + } + return p +} + +// Equal checks equality between two polynomials +func (p *Polynomial) Equal(p1 Polynomial) bool { + if (*p == nil) != (p1 == nil) { + return false + } + + if len(*p) != len(p1) { + return false + } + + for i := range p1 { + if !(*p)[i].Equal(&p1[i]) { + return false + } + } + + return true +} + +func (p Polynomial) SetZero() { + for i := 0; i < len(p); i++ { + p[i].SetZero() + } +} + +func (p Polynomial) Text(base int) string { + + var builder strings.Builder + + first := true + for d := len(p) - 1; d >= 0; d-- { + if p[d].IsZero() { + continue + } + + pD := p[d] + pDText := pD.Text(base) + + initialLen := builder.Len() + + if pDText[0] == '-' { + pDText = pDText[1:] + if first { + builder.WriteString("-") + } else { + builder.WriteString(" - ") + } + } else if !first { + builder.WriteString(" + ") + } + + first = false + + if !pD.IsOne() || d == 0 { + builder.WriteString(pDText) + } + + if builder.Len()-initialLen > 10 { + builder.WriteString("×") + } + + if d != 0 { + builder.WriteString("X") + } + if d > 1 { + builder.WriteString( + utils.ToSuperscript(strconv.Itoa(d)), + ) + } + + } + + if first { + return "0" + } + + return builder.String() +} + +// InterpolateOnRange maps vector v to polynomial f +// such that f(i) = v[i] for 0 ≤ i < len(v). +// len(f) = len(v) and deg(f) ≤ len(v) - 1 +func InterpolateOnRange(v []small_rational.SmallRational) Polynomial { + nEvals := uint8(len(v)) + if int(nEvals) != len(v) { + panic("interpolation method too inefficient for nEvals > 255") + } + lagrange := getLagrangeBasis(nEvals) + + var res Polynomial + res.Scale(&v[0], lagrange[0]) + + temp := make(Polynomial, nEvals) + + for i := uint8(1); i < nEvals; i++ { + temp.Scale(&v[i], lagrange[i]) + res.Add(res, temp) + } + + return res +} + +// lagrange bases used by InterpolateOnRange +var lagrangeBasis sync.Map + +func getLagrangeBasis(domainSize uint8) []Polynomial { + if res, ok := lagrangeBasis.Load(domainSize); ok { + return res.([]Polynomial) + } + + // not found. compute + var res []Polynomial + if domainSize >= 2 { + res = computeLagrangeBasis(domainSize) + } else if domainSize == 1 { + res = []Polynomial{make(Polynomial, 1)} + res[0][0].SetOne() + } + lagrangeBasis.Store(domainSize, res) + + return res +} + +// computeLagrangeBasis precomputes in explicit coefficient form for each 0 ≤ l < domainSize the polynomial +// pₗ := X (X-1) ... (X-l-1) (X-l+1) ... (X - domainSize + 1) / ( l (l-1) ... 2 (-1) ... (l - domainSize +1) ) +// Note that pₗ(l) = 1 and pₗ(n) = 0 if 0 ≤ l < domainSize, n ≠ l +func computeLagrangeBasis(domainSize uint8) []Polynomial { + + constTerms := make([]small_rational.SmallRational, domainSize) + for i := uint8(0); i < domainSize; i++ { + constTerms[i].SetInt64(-int64(i)) + } + + res := make([]Polynomial, domainSize) + multScratch := make(Polynomial, domainSize-1) + + // compute pₗ + for l := uint8(0); l < domainSize; l++ { + + // TODO @Tabaie Optimize this with some trees? O(log(domainSize)) polynomial mults instead of O(domainSize)? Then again it would be fewer big poly mults vs many small poly mults + d := uint8(0) //d is the current degree of res + for i := uint8(0); i < domainSize; i++ { + if i == l { + continue + } + if d == 0 { + res[l] = make(Polynomial, domainSize) + res[l][domainSize-2] = constTerms[i] + res[l][domainSize-1].SetOne() + } else { + current := res[l][domainSize-d-2:] + timesConst := multScratch[domainSize-d-2:] + + timesConst.Scale(&constTerms[i], current[1:]) //TODO: Directly double and add since constTerms are tiny? (even less than 4 bits) + nonLeading := current[0 : d+1] + + nonLeading.Add(nonLeading, timesConst) + + } + d++ + } + + } + + // We have pₗ(i≠l)=0. Now scale so that pₗ(l)=1 + // Replace the constTerms with norms + for l := uint8(0); l < domainSize; l++ { + constTerms[l].Neg(&constTerms[l]) + constTerms[l] = res[l].Eval(&constTerms[l]) + } + constTerms = small_rational.BatchInvert(constTerms) + for l := uint8(0); l < domainSize; l++ { + res[l].ScaleInPlace(&constTerms[l]) + } + + return res +} diff --git a/internal/small_rational/polynomial/pool.go b/internal/small_rational/polynomial/pool.go new file mode 100644 index 00000000..bc855ef5 --- /dev/null +++ b/internal/small_rational/polynomial/pool.go @@ -0,0 +1,29 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +package polynomial + +import ( + "github.com/consensys/gnark/internal/small_rational" +) + +// Do as little as possible to instantiate the interface +type Pool struct { +} + +func NewPool(...int) (pool Pool) { + return Pool{} +} + +func (p *Pool) Make(n int) []small_rational.SmallRational { + return make([]small_rational.SmallRational, n) +} + +func (p *Pool) Dump(...[]small_rational.SmallRational) { +} + +func (p *Pool) Clone(slice []small_rational.SmallRational) []small_rational.SmallRational { + res := p.Make(len(slice)) + copy(res, slice) + return res +} diff --git a/internal/small_rational/small-rational.go b/internal/small_rational/small-rational.go new file mode 100644 index 00000000..6dbd87f1 --- /dev/null +++ b/internal/small_rational/small-rational.go @@ -0,0 +1,459 @@ +package small_rational + +import ( + "crypto/rand" + "fmt" + "math/big" + "strconv" + "strings" +) + +const Bytes = 64 + +// SmallRational implements the rational field, used to generate field agnostic test vectors. +// It is not optimized for performance, so it is best used sparingly. +type SmallRational struct { + text string //For debugging purposes + numerator big.Int + denominator big.Int // By convention, denominator == 0 also indicates zero +} + +var smallPrimes = []*big.Int{ + big.NewInt(2), big.NewInt(3), big.NewInt(5), + big.NewInt(7), big.NewInt(11), big.NewInt(13), +} + +func bigDivides(p, a *big.Int) bool { + var remainder big.Int + remainder.Mod(a, p) + return remainder.BitLen() == 0 +} + +func (z *SmallRational) UpdateText() { + z.text = z.Text(10) +} + +func (z *SmallRational) simplify() { + + if z.numerator.BitLen() == 0 || z.denominator.BitLen() == 0 { + return + } + + var num, den big.Int + + num.Set(&z.numerator) + den.Set(&z.denominator) + + for _, p := range smallPrimes { + for bigDivides(p, &num) && bigDivides(p, &den) { + num.Div(&num, p) + den.Div(&den, p) + } + } + + if bigDivides(&den, &num) { + num.Div(&num, &den) + den.SetInt64(1) + } + + z.numerator = num + z.denominator = den + +} +func (z *SmallRational) Square(x *SmallRational) *SmallRational { + var num, den big.Int + num.Mul(&x.numerator, &x.numerator) + den.Mul(&x.denominator, &x.denominator) + + z.numerator = num + z.denominator = den + + z.UpdateText() + + return z +} + +func (z *SmallRational) String() string { + z.text = z.Text(10) + return z.text +} + +func (z *SmallRational) Add(x, y *SmallRational) *SmallRational { + if x.denominator.BitLen() == 0 { + *z = *y + } else if y.denominator.BitLen() == 0 { + *z = *x + } else { + //TODO: Exploit cases where one denom divides the other + var numDen, denNum big.Int + numDen.Mul(&x.numerator, &y.denominator) + denNum.Mul(&x.denominator, &y.numerator) + + numDen.Add(&denNum, &numDen) + z.numerator = numDen //to avoid shallow copy problems + + denNum.Mul(&x.denominator, &y.denominator) + z.denominator = denNum + z.simplify() + } + + z.UpdateText() + + return z +} + +func (z *SmallRational) IsZero() bool { + return z.numerator.BitLen() == 0 || z.denominator.BitLen() == 0 +} + +func (z *SmallRational) Inverse(x *SmallRational) *SmallRational { + if x.IsZero() { + *z = *x + } else { + *z = SmallRational{numerator: x.denominator, denominator: x.numerator} + z.UpdateText() + } + + return z +} + +func (z *SmallRational) Neg(x *SmallRational) *SmallRational { + z.numerator.Neg(&x.numerator) + z.denominator = x.denominator + + if x.text == "" { + x.UpdateText() + } + + if x.text[0] == '-' { + z.text = x.text[1:] + } else { + z.text = "-" + x.text + } + + return z +} + +func (z *SmallRational) Double(x *SmallRational) *SmallRational { + + var y big.Int + + if x.denominator.Bit(0) == 0 { + z.numerator = x.numerator + y.Rsh(&x.denominator, 1) + z.denominator = y + } else { + y.Lsh(&x.numerator, 1) + z.numerator = y + z.denominator = x.denominator + } + + z.UpdateText() + + return z +} + +func (z *SmallRational) Sign() int { + return z.numerator.Sign() * z.denominator.Sign() +} + +func (z *SmallRational) MarshalJSON() ([]byte, error) { + return []byte(z.String()), nil +} + +func (z *SmallRational) UnmarshalJson(data []byte) error { + _, err := z.SetInterface(string(data)) + return err +} + +func (z *SmallRational) Equal(x *SmallRational) bool { + return z.Cmp(x) == 0 +} + +func (z *SmallRational) Sub(x, y *SmallRational) *SmallRational { + var yNeg SmallRational + yNeg.Neg(y) + z.Add(x, &yNeg) + + z.UpdateText() + return z +} + +func (z *SmallRational) Cmp(x *SmallRational) int { + zSign, xSign := z.Sign(), x.Sign() + + if zSign > xSign { + return 1 + } + if zSign < xSign { + return -1 + } + + var Z, X big.Int + Z.Mul(&z.numerator, &x.denominator) + X.Mul(&x.numerator, &z.denominator) + + Z.Abs(&Z) + X.Abs(&X) + + return Z.Cmp(&X) * zSign + +} + +func BatchInvert(a []SmallRational) []SmallRational { + res := make([]SmallRational, len(a)) + for i := range a { + res[i].Inverse(&a[i]) + } + return res +} + +func (z *SmallRational) Mul(x, y *SmallRational) *SmallRational { + var num, den big.Int + + num.Mul(&x.numerator, &y.numerator) + den.Mul(&x.denominator, &y.denominator) + + z.numerator = num + z.denominator = den + + z.simplify() + z.UpdateText() + return z +} + +func (z *SmallRational) Div(x, y *SmallRational) *SmallRational { + var num, den big.Int + + num.Mul(&x.numerator, &y.denominator) + den.Mul(&x.denominator, &y.numerator) + + z.numerator = num + z.denominator = den + + z.simplify() + z.UpdateText() + return z +} + +func (z *SmallRational) Halve() *SmallRational { + if z.numerator.Bit(0) == 0 { + z.numerator.Rsh(&z.numerator, 1) + } else { + z.denominator.Lsh(&z.denominator, 1) + } + + z.simplify() + z.UpdateText() + return z +} + +func (z *SmallRational) SetOne() *SmallRational { + return z.SetInt64(1) +} + +func (z *SmallRational) SetZero() *SmallRational { + return z.SetInt64(0) +} + +func (z *SmallRational) SetInt64(i int64) *SmallRational { + z.numerator = *big.NewInt(i) + z.denominator = *big.NewInt(1) + z.text = strconv.FormatInt(i, 10) + return z +} + +func (z *SmallRational) SetRandom() (*SmallRational, error) { + + bytes := make([]byte, 1) + n, err := rand.Read(bytes) + if err != nil { + return nil, err + } + if n != len(bytes) { + return nil, fmt.Errorf("%d bytes read instead of %d", n, len(bytes)) + } + + z.numerator = *big.NewInt(int64(bytes[0]%16) - 8) + z.denominator = *big.NewInt(int64((bytes[0]) / 16)) + + z.simplify() + z.UpdateText() + + return z, nil +} + +func (z *SmallRational) MustSetRandom() *SmallRational { + if _, err := z.SetRandom(); err != nil { + panic(err) + } + return z +} + +func (z *SmallRational) SetUint64(i uint64) { + var num big.Int + num.SetUint64(i) + z.numerator = num + z.denominator = *big.NewInt(1) + z.text = strconv.FormatUint(i, 10) +} + +func (z *SmallRational) IsOne() bool { + return z.numerator.Cmp(&z.denominator) == 0 && z.denominator.BitLen() != 0 +} + +func (z *SmallRational) Text(base int) string { + + if z.denominator.BitLen() == 0 { + return "0" + } + + if z.denominator.Sign() < 0 { + var num, den big.Int + num.Neg(&z.numerator) + den.Neg(&z.denominator) + z.numerator = num + z.denominator = den + } + + if bigDivides(&z.denominator, &z.numerator) { + var num big.Int + num.Div(&z.numerator, &z.denominator) + z.numerator = num + z.denominator = *big.NewInt(1) + } + + numerator := z.numerator.Text(base) + + if z.denominator.IsInt64() && z.denominator.Int64() == 1 { + return numerator + } + + return numerator + "/" + z.denominator.Text(base) +} + +func (z *SmallRational) Set(x *SmallRational) *SmallRational { + *z = *x // shallow copy is safe because ops are never in place + return z +} + +func (z *SmallRational) SetInterface(x interface{}) (*SmallRational, error) { + + switch v := x.(type) { + case *SmallRational: + *z = *v + case SmallRational: + *z = v + case int64: + z.SetInt64(v) + case int: + z.SetInt64(int64(v)) + case float64: + asInt := int64(v) + if float64(asInt) != v { + return nil, fmt.Errorf("cannot currently parse float") + } + z.SetInt64(asInt) + case string: + z.text = v + sep := strings.Split(v, "/") + switch len(sep) { + case 1: + if asInt, err := strconv.Atoi(sep[0]); err == nil { + z.SetInt64(int64(asInt)) + } else { + return nil, err + } + case 2: + var err error + var num, denom int + num, err = strconv.Atoi(sep[0]) + if err != nil { + return nil, err + } + denom, err = strconv.Atoi(sep[1]) + if err != nil { + return nil, err + } + z.numerator = *big.NewInt(int64(num)) + z.denominator = *big.NewInt(int64(denom)) + default: + return nil, fmt.Errorf("cannot parse \"%s\"", v) + } + default: + return nil, fmt.Errorf("cannot parse %T", x) + } + + return z, nil +} + +func bigIntToBytesSigned(dst []byte, src big.Int) { + src.FillBytes(dst[1:]) + dst[0] = 0 + if src.Sign() < 0 { + dst[0] = 255 + } +} + +func (z *SmallRational) Bytes() [Bytes]byte { + var res [Bytes]byte + bigIntToBytesSigned(res[:Bytes/2], z.numerator) + bigIntToBytesSigned(res[Bytes/2:], z.denominator) + return res +} + +func (z *SmallRational) Marshal() []byte { + res := z.Bytes() + return res[:] +} + +func bytesToBigIntSigned(src []byte) big.Int { + var res big.Int + res.SetBytes(src[1:]) + if src[0] != 0 { + res.Neg(&res) + } + return res +} + +// BigInt returns sets dst to the value of z if it is an integer. +// if z is not an integer, nil is returned. +// if the given dst is nil, the address of the numerator is returned. +// if the given dst is non-nil, it is returned. +func (z *SmallRational) BigInt(dst *big.Int) *big.Int { + if z.denominator.Cmp(big.NewInt(1)) != 0 { + return nil + } + if dst == nil { + return &z.numerator + } + dst.Set(&z.numerator) + return dst +} + +func (z *SmallRational) SetBytes(b []byte) { + if len(b) > Bytes/2 { + z.numerator = bytesToBigIntSigned(b[:Bytes/2]) + z.denominator = bytesToBigIntSigned(b[Bytes/2:]) + } else { + z.numerator.SetBytes(b) + z.denominator.SetInt64(1) + } + z.simplify() + z.UpdateText() +} + +func One() SmallRational { + res := SmallRational{ + text: "1", + } + res.numerator.SetInt64(1) + res.denominator.SetInt64(1) + return res +} + +func Modulus() *big.Int { + res := big.NewInt(1) + res.Lsh(res, 64) + return res +} diff --git a/internal/small_rational/small_rational_test.go b/internal/small_rational/small_rational_test.go new file mode 100644 index 00000000..3db3ccab --- /dev/null +++ b/internal/small_rational/small_rational_test.go @@ -0,0 +1,116 @@ +package small_rational + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBigDivides(t *testing.T) { + assert.True(t, bigDivides(big.NewInt(-1), big.NewInt(4))) + assert.False(t, bigDivides(big.NewInt(-3), big.NewInt(4))) +} + +func TestCmp(t *testing.T) { + + cases := make([]SmallRational, 36) + + for i := int64(0); i < 9; i++ { + if i%2 == 0 { + cases[4*i].numerator.SetInt64((i - 4) / 2) + cases[4*i].denominator.SetInt64(1) + } else { + cases[4*i].numerator.SetInt64(i - 4) + cases[4*i].denominator.SetInt64(2) + } + + cases[4*i+1].numerator.Neg(&cases[4*i].numerator) + cases[4*i+1].denominator.Neg(&cases[4*i].denominator) + + cases[4*i+2].numerator.Lsh(&cases[4*i].numerator, 1) + cases[4*i+2].denominator.Lsh(&cases[4*i].denominator, 1) + + cases[4*i+3].numerator.Neg(&cases[4*i+2].numerator) + cases[4*i+3].denominator.Neg(&cases[4*i+2].denominator) + } + + for i := range cases { + for j := range cases { + I, J := i/4, j/4 + var expectedCmp int + cmp := cases[i].Cmp(&cases[j]) + if I < J { + expectedCmp = -1 + } else if I == J { + expectedCmp = 0 + } else { + expectedCmp = 1 + } + assert.Equal(t, expectedCmp, cmp, "comparing index %d, index %d", i, j) + } + } + + zeroIndex := len(cases) / 8 + var weirdZero SmallRational + for i := range cases { + I := i / 4 + var expectedCmp int + cmp := cases[i].Cmp(&weirdZero) + cmpNeg := weirdZero.Cmp(&cases[i]) + if I < zeroIndex { + expectedCmp = -1 + } else if I == zeroIndex { + expectedCmp = 0 + } else { + expectedCmp = 1 + } + + assert.Equal(t, expectedCmp, cmp, "comparing index %d, 0/0", i) + assert.Equal(t, -expectedCmp, cmpNeg, "comparing 0/0, index %d", i) + } +} + +func TestDouble(t *testing.T) { + values := []interface{}{1, 2, 3, 4, 5, "2/3", "3/2", "-3/-2"} + valsDoubled := []interface{}{2, 4, 6, 8, 10, "-4/-3", 3, 3} + + for i := range values { + var v, vDoubled, vDoubledExpected SmallRational + _, err := v.SetInterface(values[i]) + assert.NoError(t, err) + _, err = vDoubledExpected.SetInterface(valsDoubled[i]) + assert.NoError(t, err) + vDoubled.Double(&v) + assert.True(t, vDoubled.Equal(&vDoubledExpected), + "mismatch at %d: expected 2×%s = %s, saw %s", i, v.text, vDoubledExpected.text, vDoubled.text) + + } +} + +func TestOperandConstancy(t *testing.T) { + var p0, p, pPure SmallRational + p0.SetInt64(1) + p.SetInt64(-3) + pPure.SetInt64(-3) + + res := p + res.Add(&res, &p0) + assert.True(t, p.Equal(&pPure)) +} + +func TestSquare(t *testing.T) { + var two, four, x SmallRational + two.SetInt64(2) + four.SetInt64(4) + + x.Square(&two) + + assert.True(t, x.Equal(&four), "expected 4, saw %s", x.Text(10)) +} + +func TestSetBytes(t *testing.T) { + var c SmallRational + c.SetBytes([]byte("firstChallenge.0")) + +} diff --git a/internal/small_rational/vector.go b/internal/small_rational/vector.go new file mode 100644 index 00000000..07fcc3af --- /dev/null +++ b/internal/small_rational/vector.go @@ -0,0 +1,9 @@ +package small_rational + +type Vector []SmallRational + +func (v Vector) MustSetRandom() { + for i := range v { + v[i].MustSetRandom() + } +} diff --git a/internal/smallfields/circuits_test.go b/internal/smallfields/circuits_test.go new file mode 100644 index 00000000..aeaa6093 --- /dev/null +++ b/internal/smallfields/circuits_test.go @@ -0,0 +1,220 @@ +package smallfields_test + +import ( + "crypto/rand" + "fmt" + "math/big" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/bn254" + "github.com/consensys/gnark-crypto/field/babybear" + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/frontend/cs/scs" + "github.com/consensys/gnark/internal/smallfields/tinyfield" + "github.com/consensys/gnark/internal/widecommitter" + "github.com/consensys/gnark/std/algebra/emulated/sw_bn254" + "github.com/consensys/gnark/std/math/emulated" + "github.com/consensys/gnark/std/math/emulated/emparams" + "github.com/consensys/gnark/test" +) + +var testSmallField = koalabear.Modulus() + +type NativeCircuit struct { + A frontend.Variable `gnark:",public"` + B frontend.Variable `gnark:",secret"` +} + +func (circuit *NativeCircuit) Define(api frontend.API) error { + res := api.Mul(circuit.A, circuit.A) + api.AssertIsEqual(res, circuit.B) + return nil +} + +var testCases = []struct { + name string + modulus *big.Int + supportsCompile bool +}{ + {"tinyfield", tinyfield.Modulus(), true}, + {"babybear", babybear.Modulus(), true}, + {"koalabear", koalabear.Modulus(), true}, +} + +func TestNativeCircuitTestSolve(t *testing.T) { + assert := test.NewAssert(t) + for _, tc := range testCases { + assert.Run(func(assert *test.Assert) { + err := test.IsSolved(&NativeCircuit{}, &NativeCircuit{A: 2, B: 4}, tc.modulus) + assert.NoError(err) + }, tc.name) + } +} + +func TestNativeCircuitCompileAndSolve(t *testing.T) { + assert := test.NewAssert(t) + for _, tc := range testCases { + if !tc.supportsCompile { + continue + } + assert.Run(func(assert *test.Assert) { + ccs, err := frontend.CompileU32(tc.modulus, r1cs.NewBuilder, &NativeCircuit{}) + assert.NoError(err) + assignment := &NativeCircuit{A: 2, B: 4} + wit, err := frontend.NewWitness(assignment, tc.modulus) + assert.NoError(err) + err = ccs.IsSolved(wit) + assert.NoError(err) + + }, fmt.Sprintf("ccs=r1cs/field=%s", tc.name)) + assert.Run(func(assert *test.Assert) { + ccs, err := frontend.CompileU32(tc.modulus, scs.NewBuilder, &NativeCircuit{}) + assert.NoError(err) + assignment := &NativeCircuit{A: 2, B: 4} + wit, err := frontend.NewWitness(assignment, tc.modulus) + assert.NoError(err) + err = ccs.IsSolved(wit) + assert.NoError(err) + }, fmt.Sprintf("ccs=scs/field=%s", tc.name)) + } +} + +type EmulatedCircuit[T emulated.FieldParams] struct { + A emulated.Element[T] `gnark:",public"` + B emulated.Element[T] `gnark:",secret"` +} + +func (c *EmulatedCircuit[T]) Define(api frontend.API) error { + f, err := emulated.NewField[T](api) + if err != nil { + return err + } + res := f.Mul(&c.A, &c.A) + f.AssertIsEqual(res, &c.B) + return nil +} + +func TestEmulatedCircuit(t *testing.T) { + assert := test.NewAssert(t) + + a, err := rand.Int(rand.Reader, emparams.BN254Fp{}.Modulus()) + assert.NoError(err) + b := new(big.Int).Mul(a, a) + b.Mod(b, emparams.BN254Fp{}.Modulus()) + + err = test.IsSolved(&EmulatedCircuit[emparams.BN254Fp]{}, &EmulatedCircuit[emparams.BN254Fp]{A: emulated.ValueOf[emparams.BN254Fp](a), B: emulated.ValueOf[emparams.BN254Fp](b)}, ecc.BN254.ScalarField()) + assert.NoError(err) + + err = test.IsSolved(&EmulatedCircuit[emparams.BN254Fp]{}, &EmulatedCircuit[emparams.BN254Fp]{A: emulated.ValueOf[emparams.BN254Fp](a), B: emulated.ValueOf[emparams.BN254Fp](b)}, testSmallField) + assert.NoError(err) + + // assert that when the compiled doesn't have specific support for small fields (rangechecker and widecommit), then it would fail + err = test.IsSolved(&EmulatedCircuit[emparams.BN254Fp]{}, &EmulatedCircuit[emparams.BN254Fp]{A: emulated.ValueOf[emparams.BN254Fp](a), B: emulated.ValueOf[emparams.BN254Fp](b)}, testSmallField, test.WithNoSmallFieldCompatibility()) + assert.Error(err) +} + +func TestCompileEmulatedCircuit(t *testing.T) { + assert := test.NewAssert(t) + f := testSmallField + + assignment := &EmulatedCircuit[emparams.BN254Fp]{A: emulated.ValueOf[emparams.BN254Fp](2), B: emulated.ValueOf[emparams.BN254Fp](4)} + + ccs, err := frontend.CompileU32(f, widecommitter.From(scs.NewBuilder), &EmulatedCircuit[emparams.BN254Fp]{}) + assert.NoError(err) + + w, err := frontend.NewWitness(assignment, f) + assert.NoError(err) + + err = ccs.IsSolved(w) + assert.NoError(err) + + ccs2, err := frontend.CompileU32(f, widecommitter.From(r1cs.NewBuilder), &EmulatedCircuit[emparams.BN254Fp]{}) + assert.NoError(err) + + err = ccs2.IsSolved(w) + assert.NoError(err) + + // ensure the compilation fails in case we compile over a small field but we don't have rangechecker and widecommit support + _, err = frontend.CompileU32(f, scs.NewBuilder, &EmulatedCircuit[emparams.BN254Fp]{}) + assert.Error(err) + _, err = frontend.CompileU32(f, r1cs.NewBuilder, &EmulatedCircuit[emparams.BN254Fp]{}) + assert.Error(err) +} + +type PairCircuit struct { + InG1 sw_bn254.G1Affine + InG2 sw_bn254.G2Affine + Res sw_bn254.GTEl +} + +func (c *PairCircuit) Define(api frontend.API) error { + pairing, err := sw_bn254.NewPairing(api) + if err != nil { + return fmt.Errorf("new pairing: %w", err) + } + pairing.AssertIsOnG1(&c.InG1) + pairing.AssertIsOnG2(&c.InG2) + res, err := pairing.Pair([]*sw_bn254.G1Affine{&c.InG1}, []*sw_bn254.G2Affine{&c.InG2}) + if err != nil { + return fmt.Errorf("pair: %w", err) + } + pairing.AssertIsEqual(res, &c.Res) + return nil +} + +func TestPairTestSolve(t *testing.T) { + assert := test.NewAssert(t) + p, q := randomG1G2Affines() + res, err := bn254.Pair([]bn254.G1Affine{p}, []bn254.G2Affine{q}) + assert.NoError(err) + witness := PairCircuit{ + InG1: sw_bn254.NewG1Affine(p), + InG2: sw_bn254.NewG2Affine(q), + Res: sw_bn254.NewGTEl(res), + } + err = test.IsSolved(&PairCircuit{}, &witness, testSmallField) + assert.NoError(err) + + ccs, err := frontend.CompileU32(testSmallField, widecommitter.From(scs.NewBuilder), &PairCircuit{}) + assert.NoError(err) + + w, err := frontend.NewWitness(&witness, testSmallField) + assert.NoError(err) + + err = ccs.IsSolved(w) + assert.NoError(err) + + ccs2, err := frontend.Compile(ecc.BLS12_377.ScalarField(), scs.NewBuilder, &PairCircuit{}) + assert.NoError(err) + // we define it again as the field is different + witness = PairCircuit{ + InG1: sw_bn254.NewG1Affine(p), + InG2: sw_bn254.NewG2Affine(q), + Res: sw_bn254.NewGTEl(res), + } + w2, err := frontend.NewWitness(&witness, ecc.BLS12_377.ScalarField()) + assert.NoError(err) + err = ccs2.IsSolved(w2) + assert.NoError(err) +} + +func randomG1G2Affines() (bn254.G1Affine, bn254.G2Affine) { + _, _, G1AffGen, G2AffGen := bn254.Generators() + mod := bn254.ID.ScalarField() + s1, err := rand.Int(rand.Reader, mod) + if err != nil { + panic(err) + } + s2, err := rand.Int(rand.Reader, mod) + if err != nil { + panic(err) + } + var p bn254.G1Affine + p.ScalarMultiplication(&G1AffGen, s1) + var q bn254.G2Affine + q.ScalarMultiplication(&G2AffGen, s2) + return p, q +} diff --git a/internal/smallfields/smallfield_assert.go b/internal/smallfields/smallfield_assert.go new file mode 100644 index 00000000..b5f2f1db --- /dev/null +++ b/internal/smallfields/smallfield_assert.go @@ -0,0 +1,33 @@ +package smallfields + +import ( + "math/big" + + "github.com/consensys/gnark-crypto/field/babybear" + "github.com/consensys/gnark-crypto/field/koalabear" + "github.com/consensys/gnark/internal/smallfields/tinyfield" +) + +// IsSmallField returns true if the field is a small field. Small fields do not +// support pairing based backends, but are useful for testing and exporting to +// other proof systems. +func IsSmallField(field *big.Int) bool { + for _, f := range Supported() { + if field.Cmp(f) == 0 { + return true + } + } + return false +} + +// Supported returns the list of supported small fields. Currently we support: +// - babybear +// - koalabear +// - tinyfield -- experimental very small field for fuzzing purposes +func Supported() []*big.Int { + return []*big.Int{ + babybear.Modulus(), + koalabear.Modulus(), + tinyfield.Modulus(), + } +} diff --git a/internal/tinyfield/doc.go b/internal/smallfields/tinyfield/doc.go similarity index 94% rename from internal/tinyfield/doc.go rename to internal/smallfields/tinyfield/doc.go index a22f71f7..5f392802 100644 --- a/internal/tinyfield/doc.go +++ b/internal/smallfields/tinyfield/doc.go @@ -7,13 +7,13 @@ // // The API is similar to math/big (big.Int), but the operations are significantly faster (up to 20x). // -// Additionally tinyfield.Vector offers an API to manipulate []Element. +// Additionally tinyfield.Vector offers an API to manipulate []Element using AVX512/NEON instructions if available. // // The modulus is hardcoded in all the operations. // // Field elements are represented as an array, and assumed to be in Montgomery form in all methods: // -// type Element [1]uint64 +// type Element [1]uint32 // // # Usage // diff --git a/internal/tinyfield/element.go b/internal/smallfields/tinyfield/element.go similarity index 89% rename from internal/tinyfield/element.go rename to internal/smallfields/tinyfield/element.go index dcaa56b2..67c17af9 100644 --- a/internal/tinyfield/element.go +++ b/internal/smallfields/tinyfield/element.go @@ -21,7 +21,7 @@ import ( "github.com/consensys/gnark-crypto/field/pool" ) -// Element represents a field element stored on 1 words (uint64) +// Element represents a field element stored on 1 words (uint32) // // Element are assumed to be in Montgomery form in all methods. // @@ -33,12 +33,12 @@ import ( // # Warning // // This code has not been audited and is provided as-is. In particular, there is no security guarantees such as constant time implementation or side-channel attack resistance. -type Element [1]uint64 +type Element [1]uint32 const ( - Limbs = 1 // number of 64 bits words needed to represent a Element + Limbs = 1 // number of 32 bits words needed to represent a Element Bits = 6 // number of bits needed to represent a Element - Bytes = 8 // number of bytes needed to represent a Element + Bytes = 4 // number of bytes needed to represent a Element ) // Field modulus q @@ -63,7 +63,7 @@ func Modulus() *big.Int { // q + r'.r = 1, i.e., qInvNeg = - q⁻¹ mod r // used for Montgomery reduction -const qInvNeg = 12559485326780971313 +const qInvNeg = 2558703921 func init() { _modulus.SetString("2f", 16) @@ -76,16 +76,16 @@ func init() { // var v Element // v.SetUint64(...) func NewElement(v uint64) Element { - z := Element{v} - z.Mul(&z, &rSquare) + z := Element{uint32(v % uint64(q0))} + z.toMont() return z } // SetUint64 sets z to v and returns z func (z *Element) SetUint64(v uint64) *Element { // sets z LSB to v (non-Montgomery form) and convert z to Montgomery form - *z = Element{v} - return z.Mul(z, &rSquare) // z.toMont() + *z = Element{uint32(v % uint64(q0))} + return z.toMont() } // SetInt64 sets z to v and returns z @@ -178,7 +178,7 @@ func (z *Element) SetZero() *Element { // SetOne z = 1 (in Montgomery form) func (z *Element) SetOne() *Element { - z[0] = 25 + z[0] = 42 return z } @@ -196,7 +196,7 @@ func (z *Element) Equal(x *Element) bool { } // NotEqual returns 0 if and only if z == x; constant-time -func (z *Element) NotEqual(x *Element) uint64 { +func (z *Element) NotEqual(x *Element) uint32 { return (z[0] ^ x[0]) } @@ -207,7 +207,7 @@ func (z *Element) IsZero() bool { // IsOne returns z == 1 func (z *Element) IsOne() bool { - return z[0] == 25 + return z[0] == 42 } // IsUint64 reports whether z can be represented as an uint64. @@ -217,7 +217,7 @@ func (z *Element) IsUint64() bool { // Uint64 returns the uint64 representation of x. If x cannot be represented in a uint64, the result is undefined. func (z *Element) Uint64() uint64 { - return z.Bits()[0] + return uint64(z.Bits()[0]) } // FitsOnOneWord reports whether z words (except the least significant word) are 0 @@ -252,8 +252,8 @@ func (z *Element) LexicographicallyLargest() bool { _z := z.Bits() - var b uint64 - _, b = bits.Sub64(_z[0], 24, 0) + var b uint32 + _, b = bits.Sub32(_z[0], 24, 0) return b == 0 } @@ -292,7 +292,7 @@ func (z *Element) SetRandom() (*Element, error) { // Clear unused bits in in the most significant byte to increase probability // that the candidate is < q. bytes[k-1] &= uint8(int(1<> 1 @@ -348,26 +358,21 @@ func (z *Element) Add(x, y *Element) *Element { // Double z = x + x (mod q), aka Lsh 1 func (z *Element) Double(x *Element) *Element { - if x[0]&(1<<63) == (1 << 63) { - // if highest bit is set, then we have a carry to x + x, we shift and subtract q - z[0] = (x[0] << 1) - q - } else { - // highest bit is not set, but x + x can still be >= q - z[0] = (x[0] << 1) - if z[0] >= q { - z[0] -= q - } + t := x[0] << 1 + if t >= q { + t -= q } + z[0] = t return z } // Sub z = x - y (mod q) func (z *Element) Sub(x, y *Element) *Element { - var b uint64 - z[0], b = bits.Sub64(x[0], y[0], 0) + t, b := bits.Sub32(x[0], y[0], 0) if b != 0 { - z[0] += q + t += q } + z[0] = t return z } @@ -384,26 +389,13 @@ func (z *Element) Neg(x *Element) *Element { // Select is a constant-time conditional move. // If c=0, z = x0. Else z = x1 func (z *Element) Select(c int, x0 *Element, x1 *Element) *Element { - cC := uint64((int64(c) | -int64(c)) >> 63) // "canonicized" into: 0 if c=0, -1 otherwise + cC := uint32((int64(c) | -int64(c)) >> 63) // "canonicized" into: 0 if c=0, -1 otherwise z[0] = x0[0] ^ cC&(x0[0]^x1[0]) return z } func _fromMontGeneric(z *Element) { - // the following lines implement z = z * 1 - // with a modified CIOS montgomery multiplication - // see Mul for algorithm documentation - { - // m = z[0]n'[0] mod W - m := z[0] * qInvNeg - C := madd0(m, q0, z[0]) - z[0] = C - } - - // if z ⩾ q → z -= q - if !z.smallerThanModulus() { - z[0] -= q - } + z[0] = montReduce(uint64(z[0])) } func _reduceGeneric(z *Element) { @@ -456,7 +448,7 @@ func _butterflyGeneric(a, b *Element) { // BitLen returns the minimum number of bits needed to represent z // returns 0 if z == 0 func (z *Element) BitLen() int { - return bits.Len64(z[0]) + return bits.Len32(z[0]) } // Hash msg to count prime field elements. @@ -523,13 +515,15 @@ func (z *Element) Exp(x Element, k *big.Int) *Element { // see section 2.3.2 of Tolga Acar's thesis // https://www.microsoft.com/en-us/research/wp-content/uploads/1998/06/97Acar.pdf var rSquare = Element{ - 14, + 25, } // toMont converts z to Montgomery form // sets and returns z = z * r² func (z *Element) toMont() *Element { - return z.Mul(z, &rSquare) + const rBits = 32 + z[0] = uint32((uint64(z[0]) << rBits) % q) + return z } // String returns the decimal representation of z as generated by @@ -541,7 +535,7 @@ func (z *Element) String() string { // toBigInt returns z as a big.Int in Montgomery form func (z *Element) toBigInt(res *big.Int) *big.Int { var b [Bytes]byte - binary.BigEndian.PutUint64(b[0:8], z[0]) + binary.BigEndian.PutUint32(b[0:4], z[0]) return res.SetBytes(b[:]) } @@ -579,10 +573,10 @@ func (z Element) ToBigIntRegular(res *big.Int) *big.Int { return z.toBigInt(res) } -// Bits provides access to z by returning its value as a little-endian [1]uint64 array. +// Bits provides access to z by returning its value as a little-endian [1]uint32 array. // Bits is intended to support implementation of missing low-level Element // functionality outside this package; it should be avoided otherwise. -func (z *Element) Bits() [1]uint64 { +func (z *Element) Bits() [1]uint32 { _z := *z fromMont(&_z) return _z @@ -631,8 +625,8 @@ func (z *Element) SetBytes(e []byte) *Element { return z } -// SetBytesCanonical interprets e as the bytes of a big-endian 8-byte integer. -// If e is not a 8-byte slice or encodes a value higher than q, +// SetBytesCanonical interprets e as the bytes of a big-endian 4-byte integer. +// If e is not a 4-byte slice or encodes a value higher than q, // SetBytesCanonical returns an error. func (z *Element) SetBytesCanonical(e []byte) error { if len(e) != Bytes { @@ -679,19 +673,9 @@ func (z *Element) SetBigInt(v *big.Int) *Element { // setBigInt assumes 0 ⩽ v < q func (z *Element) setBigInt(v *big.Int) *Element { vBits := v.Bits() - - if bits.UintSize == 64 { - for i := 0; i < len(vBits); i++ { - z[i] = uint64(vBits[i]) - } - } else { - for i := 0; i < len(vBits); i++ { - if i%2 == 0 { - z[i/2] = uint64(vBits[i]) - } else { - z[i/2] |= uint64(vBits[i]) << 32 - } - } + // we assume v < q, so even if big.Int words are on 64bits, we can safely cast them to 32bits + for i := 0; i < len(vBits); i++ { + z[i] = uint32(vBits[i]) } return z.toMont() @@ -792,11 +776,11 @@ var BigEndian bigEndian type bigEndian struct{} -// Element interpret b is a big-endian 8-byte slice. +// Element interpret b is a big-endian 4-byte slice. // If b encodes a value higher than q, Element returns error. func (bigEndian) Element(b *[Bytes]byte) (Element, error) { var z Element - z[0] = binary.BigEndian.Uint64((*b)[0:8]) + z[0] = binary.BigEndian.Uint32((*b)[0:4]) if !z.smallerThanModulus() { return Element{}, errInvalidEncoding @@ -808,7 +792,7 @@ func (bigEndian) Element(b *[Bytes]byte) (Element, error) { func (bigEndian) PutElement(b *[Bytes]byte, e Element) { e.fromMont() - binary.BigEndian.PutUint64((*b)[0:8], e[0]) + binary.BigEndian.PutUint32((*b)[0:4], e[0]) } func (bigEndian) String() string { return "BigEndian" } @@ -820,7 +804,7 @@ type littleEndian struct{} func (littleEndian) Element(b *[Bytes]byte) (Element, error) { var z Element - z[0] = binary.LittleEndian.Uint64((*b)[0:8]) + z[0] = binary.LittleEndian.Uint32((*b)[0:4]) if !z.smallerThanModulus() { return Element{}, errInvalidEncoding @@ -832,7 +816,7 @@ func (littleEndian) Element(b *[Bytes]byte) (Element, error) { func (littleEndian) PutElement(b *[Bytes]byte, e Element) { e.fromMont() - binary.LittleEndian.PutUint64((*b)[0:8], e[0]) + binary.LittleEndian.PutUint32((*b)[0:4], e[0]) } func (littleEndian) String() string { return "LittleEndian" } @@ -886,19 +870,19 @@ func (z *Element) Sqrt(x *Element) *Element { // if x == 0, sets and returns z = x func (z *Element) Inverse(x *Element) *Element { // Algorithm 16 in "Efficient Software-Implementation of Finite Fields with Applications to Cryptography" - const q uint64 = q0 + const q uint32 = q0 if x.IsZero() { z.SetZero() return z } - var r, s, u, v uint64 + var r, s, u, v uint32 u = q - s = 14 // s = r² + s = 25 // s = r² r = 0 v = x[0] - var carry, borrow uint64 + var carry, borrow uint32 for (u != 1) && (v != 1) { for v&1 == 0 { @@ -906,10 +890,10 @@ func (z *Element) Inverse(x *Element) *Element { if s&1 == 0 { s >>= 1 } else { - s, carry = bits.Add64(s, q, 0) + s, carry = bits.Add32(s, q, 0) s >>= 1 if carry != 0 { - s |= (1 << 63) + s |= (1 << 31) } } } @@ -918,22 +902,22 @@ func (z *Element) Inverse(x *Element) *Element { if r&1 == 0 { r >>= 1 } else { - r, carry = bits.Add64(r, q, 0) + r, carry = bits.Add32(r, q, 0) r >>= 1 if carry != 0 { - r |= (1 << 63) + r |= (1 << 31) } } } if v >= u { v -= u - s, borrow = bits.Sub64(s, r, 0) + s, borrow = bits.Sub32(s, r, 0) if borrow == 1 { s += q } } else { u -= v - r, borrow = bits.Sub64(r, s, 0) + r, borrow = bits.Sub32(r, s, 0) if borrow == 1 { r += q } diff --git a/internal/smallfields/tinyfield/element_purego.go b/internal/smallfields/tinyfield/element_purego.go new file mode 100644 index 00000000..f2c070d0 --- /dev/null +++ b/internal/smallfields/tinyfield/element_purego.go @@ -0,0 +1,83 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Code generated by consensys/gnark-crypto DO NOT EDIT + +package tinyfield + +// MulBy3 x *= 3 (mod q) +func MulBy3(x *Element) { + var y Element + y.SetUint64(3) + x.Mul(x, &y) +} + +// MulBy5 x *= 5 (mod q) +func MulBy5(x *Element) { + var y Element + y.SetUint64(5) + x.Mul(x, &y) +} + +// MulBy13 x *= 13 (mod q) +func MulBy13(x *Element) { + var y Element + y.SetUint64(13) + x.Mul(x, &y) +} + +// Mul2ExpNegN multiplies x by -1/2^n +// +// Since the Montgomery constant is 2^32, the Montgomery form of 1/2^n is +// 2^{32-n}. Montgomery reduction works provided the input is < 2^32 so this +// works for 0 <= n <= 32. +// +// N.B. n must be < 33. +func (z *Element) Mul2ExpNegN(x *Element, n uint32) *Element { + v := uint64(x[0]) << (32 - n) + z[0] = montReduce(v) + return z +} + +func fromMont(z *Element) { + _fromMontGeneric(z) +} + +func reduce(z *Element) { + _reduceGeneric(z) +} +func montReduce(v uint64) uint32 { + m := uint32(v) * qInvNeg + t := uint32((v + uint64(m)*q) >> 32) + if t >= q { + t -= q + } + return t +} + +// Mul z = x * y (mod q) +// +// x and y must be less than q +func (z *Element) Mul(x, y *Element) *Element { + v := uint64(x[0]) * uint64(y[0]) + z[0] = montReduce(v) + return z +} + +// Square z = x * x (mod q) +// +// x must be less than q +func (z *Element) Square(x *Element) *Element { + // see Mul for algorithm documentation + v := uint64(x[0]) * uint64(x[0]) + z[0] = montReduce(v) + return z +} + +// Butterfly sets +// +// a = a + b (mod q) +// b = a - b (mod q) +func Butterfly(a, b *Element) { + _butterflyGeneric(a, b) +} diff --git a/internal/tinyfield/element_test.go b/internal/smallfields/tinyfield/element_test.go similarity index 96% rename from internal/tinyfield/element_test.go rename to internal/smallfields/tinyfield/element_test.go index 7383f4da..abf5055b 100644 --- a/internal/tinyfield/element_test.go +++ b/internal/smallfields/tinyfield/element_test.go @@ -30,8 +30,8 @@ var benchResElement Element func BenchmarkElementSelect(b *testing.B) { var x, y Element - x.SetRandom() - y.SetRandom() + x.MustSetRandom() + y.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { @@ -41,17 +41,17 @@ func BenchmarkElementSelect(b *testing.B) { func BenchmarkElementSetRandom(b *testing.B) { var x Element - x.SetRandom() + x.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { - _, _ = x.SetRandom() + x.MustSetRandom() } } func BenchmarkElementSetBytes(b *testing.B) { var x Element - x.SetRandom() + x.MustSetRandom() bb := x.Bytes() b.ResetTimer() @@ -63,21 +63,21 @@ func BenchmarkElementSetBytes(b *testing.B) { func BenchmarkElementMulByConstants(b *testing.B) { b.Run("mulBy3", func(b *testing.B) { - benchResElement.SetRandom() + benchResElement.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { MulBy3(&benchResElement) } }) b.Run("mulBy5", func(b *testing.B) { - benchResElement.SetRandom() + benchResElement.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { MulBy5(&benchResElement) } }) b.Run("mulBy13", func(b *testing.B) { - benchResElement.SetRandom() + benchResElement.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { MulBy13(&benchResElement) @@ -87,8 +87,8 @@ func BenchmarkElementMulByConstants(b *testing.B) { func BenchmarkElementInverse(b *testing.B) { var x Element - x.SetRandom() - benchResElement.SetRandom() + x.MustSetRandom() + benchResElement.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { @@ -99,8 +99,8 @@ func BenchmarkElementInverse(b *testing.B) { func BenchmarkElementButterfly(b *testing.B) { var x Element - x.SetRandom() - benchResElement.SetRandom() + x.MustSetRandom() + benchResElement.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { Butterfly(&x, &benchResElement) @@ -109,8 +109,8 @@ func BenchmarkElementButterfly(b *testing.B) { func BenchmarkElementExp(b *testing.B) { var x Element - x.SetRandom() - benchResElement.SetRandom() + x.MustSetRandom() + benchResElement.MustSetRandom() b1, _ := rand.Int(rand.Reader, Modulus()) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -119,7 +119,7 @@ func BenchmarkElementExp(b *testing.B) { } func BenchmarkElementDouble(b *testing.B) { - benchResElement.SetRandom() + benchResElement.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { benchResElement.Double(&benchResElement) @@ -128,8 +128,8 @@ func BenchmarkElementDouble(b *testing.B) { func BenchmarkElementAdd(b *testing.B) { var x Element - x.SetRandom() - benchResElement.SetRandom() + x.MustSetRandom() + benchResElement.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { benchResElement.Add(&x, &benchResElement) @@ -138,8 +138,8 @@ func BenchmarkElementAdd(b *testing.B) { func BenchmarkElementSub(b *testing.B) { var x Element - x.SetRandom() - benchResElement.SetRandom() + x.MustSetRandom() + benchResElement.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { benchResElement.Sub(&x, &benchResElement) @@ -147,7 +147,7 @@ func BenchmarkElementSub(b *testing.B) { } func BenchmarkElementNeg(b *testing.B) { - benchResElement.SetRandom() + benchResElement.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { benchResElement.Neg(&benchResElement) @@ -156,8 +156,8 @@ func BenchmarkElementNeg(b *testing.B) { func BenchmarkElementDiv(b *testing.B) { var x Element - x.SetRandom() - benchResElement.SetRandom() + x.MustSetRandom() + benchResElement.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { benchResElement.Div(&x, &benchResElement) @@ -165,7 +165,7 @@ func BenchmarkElementDiv(b *testing.B) { } func BenchmarkElementFromMont(b *testing.B) { - benchResElement.SetRandom() + benchResElement.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { benchResElement.fromMont() @@ -173,7 +173,7 @@ func BenchmarkElementFromMont(b *testing.B) { } func BenchmarkElementSquare(b *testing.B) { - benchResElement.SetRandom() + benchResElement.MustSetRandom() b.ResetTimer() for i := 0; i < b.N; i++ { benchResElement.Square(&benchResElement) @@ -192,7 +192,7 @@ func BenchmarkElementSqrt(b *testing.B) { func BenchmarkElementMul(b *testing.B) { x := Element{ - 14, + 25, } benchResElement.SetOne() b.ResetTimer() @@ -203,7 +203,7 @@ func BenchmarkElementMul(b *testing.B) { func BenchmarkElementCmp(b *testing.B) { x := Element{ - 14, + 25, } benchResElement = x benchResElement[0] = 0 @@ -248,7 +248,7 @@ func TestElementNegZero(t *testing.T) { var a, b Element b.SetZero() for a.IsZero() { - a.SetRandom() + a.MustSetRandom() } a.Neg(&b) if !a.IsZero() { @@ -2089,6 +2089,41 @@ func TestElementJSON(t *testing.T) { assert.Equal(s, decodedS, " json with strings -> element failed") } +func TestElementMul2ExpNegN(t *testing.T) { + t.Parallel() + + parameters := gopter.DefaultTestParameters() + if testing.Short() { + parameters.MinSuccessfulTests = nbFuzzShort + } else { + parameters.MinSuccessfulTests = nbFuzz + } + + properties := gopter.NewProperties(parameters) + + genA := gen() + + properties.Property("x * 2⁻ᵏ == Mul2ExpNegN(x, k) for 0 <= k <= 32", prop.ForAll( + func(a testPairElement) bool { + + var b, e, two Element + var c [33]Element + two.SetUint64(2) + for n := 0; n < 33; n++ { + e.Exp(two, big.NewInt(int64(n))).Inverse(&e) + b.Mul(&a.element, &e) + c[n].Mul2ExpNegN(&a.element, uint32(n)) + if !c[n].Equal(&b) { + return false + } + } + return true + }, + genA, + )) + + properties.TestingRun(t, gopter.ConsoleReporter(false)) +} type testPairElement struct { element Element @@ -2100,17 +2135,17 @@ func gen() gopter.Gen { var g testPairElement g.element = Element{ - genParams.NextUint64(), + uint32(genParams.NextUint64()), } - if qElement[0] != ^uint64(0) { + if qElement[0] != ^uint32(0) { g.element[0] %= (qElement[0] + 1) } for !g.element.smallerThanModulus() { g.element = Element{ - genParams.NextUint64(), + uint32(genParams.NextUint64()), } - if qElement[0] != ^uint64(0) { + if qElement[0] != ^uint32(0) { g.element[0] %= (qElement[0] + 1) } } @@ -2125,18 +2160,18 @@ func genRandomFq(genParams *gopter.GenParameters) Element { var g Element g = Element{ - genParams.NextUint64(), + uint32(genParams.NextUint64()), } - if qElement[0] != ^uint64(0) { + if qElement[0] != ^uint32(0) { g[0] %= (qElement[0] + 1) } for !g.smallerThanModulus() { g = Element{ - genParams.NextUint64(), + uint32(genParams.NextUint64()), } - if qElement[0] != ^uint64(0) { + if qElement[0] != ^uint32(0) { g[0] %= (qElement[0] + 1) } } @@ -2148,8 +2183,8 @@ func genFull() gopter.Gen { return func(genParams *gopter.GenParameters) *gopter.GenResult { a := genRandomFq(genParams) - var carry uint64 - a[0], _ = bits.Add64(a[0], qElement[0], carry) + var carry uint32 + a[0], _ = bits.Add32(a[0], qElement[0], carry) genResult := gopter.NewGenResult(a, gopter.NoShrinker) return genResult diff --git a/internal/tinyfield/vector.go b/internal/smallfields/tinyfield/vector.go similarity index 90% rename from internal/tinyfield/vector.go rename to internal/smallfields/tinyfield/vector.go index 0755dabf..d0628898 100644 --- a/internal/tinyfield/vector.go +++ b/internal/smallfields/tinyfield/vector.go @@ -108,7 +108,7 @@ func (vector *Vector) AsyncReadFrom(r io.Reader) (int64, error, chan error) { bstart := i * Bytes bend := bstart + Bytes b := bSlice[bstart:bend] - z[0] = binary.BigEndian.Uint64(b[0:8]) + z[0] = binary.BigEndian.Uint32(b[0:4]) if !z.smallerThanModulus() { atomic.AddUint64(&cptErrors, 1) @@ -185,6 +185,30 @@ func (vector Vector) Swap(i, j int) { vector[i], vector[j] = vector[j], vector[i] } +// SetRandom sets the elements in vector to independent uniform random values in [0, q). +// +// This might error only if reading from crypto/rand.Reader errors, +// in which case the values in vector are undefined. +func (vector Vector) SetRandom() error { + for i := range vector { + if _, err := vector[i].SetRandom(); err != nil { + return err + } + } + return nil +} + +// MustSetRandom sets the elements in vector to independent uniform random values in [0, q). +// +// It panics if reading from crypto/rand.Reader errors. +func (vector Vector) MustSetRandom() { + for i := range vector { + if _, err := vector[i].SetRandom(); err != nil { + panic(err) + } + } +} + func addVecGeneric(res, a, b Vector) { if len(a) != len(b) || len(a) != len(res) { panic("vector.Add: vectors don't have the same length") diff --git a/internal/tinyfield/vector_purego.go b/internal/smallfields/tinyfield/vector_purego.go similarity index 100% rename from internal/tinyfield/vector_purego.go rename to internal/smallfields/tinyfield/vector_purego.go diff --git a/internal/tinyfield/vector_test.go b/internal/smallfields/tinyfield/vector_test.go similarity index 96% rename from internal/tinyfield/vector_test.go rename to internal/smallfields/tinyfield/vector_test.go index 36ab4f9f..4ee339a7 100644 --- a/internal/tinyfield/vector_test.go +++ b/internal/smallfields/tinyfield/vector_test.go @@ -8,12 +8,13 @@ package tinyfield import ( "bytes" "fmt" - "github.com/stretchr/testify/require" "os" "reflect" "sort" "testing" + "github.com/stretchr/testify/require" + "github.com/leanovate/gopter" "github.com/leanovate/gopter/prop" ) @@ -172,7 +173,7 @@ func TestVectorOps(t *testing.T) { return true } - sizes := []int{1, 2, 3, 4, 8, 9, 15, 16, 509, 510, 511, 512, 513, 514} + sizes := []int{1, 2, 3, 4, 8, 9, 15, 16, 24, 509, 510, 511, 512, 513, 514} type genPair struct { g1, g2 gopter.Gen label string @@ -234,7 +235,7 @@ func BenchmarkVectorOps(b *testing.B) { b1 := make(Vector, N) c1 := make(Vector, N) var mixer Element - mixer.SetRandom() + mixer.MustSetRandom() for i := 1; i < N; i++ { a1[i-1].SetUint64(uint64(i)). Mul(&a1[i-1], &mixer) @@ -328,17 +329,17 @@ func genVector(size int) gopter.Gen { return func(genParams *gopter.GenParameters) *gopter.GenResult { g := make(Vector, size) mixer := Element{ - genParams.NextUint64(), + uint32(genParams.NextUint64()), } - if qElement[0] != ^uint64(0) { + if qElement[0] != ^uint32(0) { mixer[0] %= (qElement[0] + 1) } for !mixer.smallerThanModulus() { mixer = Element{ - genParams.NextUint64(), + uint32(genParams.NextUint64()), } - if qElement[0] != ^uint64(0) { + if qElement[0] != ^uint32(0) { mixer[0] %= (qElement[0] + 1) } } diff --git a/internal/stats/latest_stats.csv b/internal/stats/latest_stats.csv index a5659381..7199f9ee 100644 --- a/internal/stats/latest_stats.csv +++ b/internal/stats/latest_stats.csv @@ -125,20 +125,20 @@ math/bits.ToTernary/unconstrained,bls24_315,plonk,160,319 math/bits.ToTernary/unconstrained,bls24_317,plonk,161,321 math/bits.ToTernary/unconstrained,bw6_761,plonk,238,475 math/bits.ToTernary/unconstrained,bw6_633,plonk,199,397 -math/emulated/secp256k1_64,bn254,groth16,1070,1950 -math/emulated/secp256k1_64,bls12_377,groth16,1070,1950 -math/emulated/secp256k1_64,bls12_381,groth16,1070,1950 -math/emulated/secp256k1_64,bls24_315,groth16,1070,1950 -math/emulated/secp256k1_64,bls24_317,groth16,1070,1950 -math/emulated/secp256k1_64,bw6_761,groth16,1070,1950 -math/emulated/secp256k1_64,bw6_633,groth16,1070,1950 -math/emulated/secp256k1_64,bn254,plonk,4497,4388 -math/emulated/secp256k1_64,bls12_377,plonk,4497,4388 -math/emulated/secp256k1_64,bls12_381,plonk,4497,4388 -math/emulated/secp256k1_64,bls24_315,plonk,4497,4388 -math/emulated/secp256k1_64,bls24_317,plonk,4497,4388 -math/emulated/secp256k1_64,bw6_761,plonk,4497,4388 -math/emulated/secp256k1_64,bw6_633,plonk,4497,4388 +math/emulated/secp256k1_64,bn254,groth16,1037,1890 +math/emulated/secp256k1_64,bls12_377,groth16,1037,1890 +math/emulated/secp256k1_64,bls12_381,groth16,1037,1890 +math/emulated/secp256k1_64,bls24_315,groth16,1037,1890 +math/emulated/secp256k1_64,bls24_317,groth16,1037,1890 +math/emulated/secp256k1_64,bw6_761,groth16,1037,1890 +math/emulated/secp256k1_64,bw6_633,groth16,1037,1890 +math/emulated/secp256k1_64,bn254,plonk,4359,4253 +math/emulated/secp256k1_64,bls12_377,plonk,4359,4253 +math/emulated/secp256k1_64,bls12_381,plonk,4359,4253 +math/emulated/secp256k1_64,bls24_315,plonk,4359,4253 +math/emulated/secp256k1_64,bls24_317,plonk,4359,4253 +math/emulated/secp256k1_64,bw6_761,plonk,4359,4253 +math/emulated/secp256k1_64,bw6_633,plonk,4359,4253 pairing_bls12377,bn254,groth16,0,0 pairing_bls12377,bls12_377,groth16,0,0 pairing_bls12377,bls12_381,groth16,0,0 @@ -153,14 +153,14 @@ pairing_bls12377,bls24_315,plonk,0,0 pairing_bls12377,bls24_317,plonk,0,0 pairing_bls12377,bw6_761,plonk,51280,51280 pairing_bls12377,bw6_633,plonk,0,0 -pairing_bls12381,bn254,groth16,947528,1567714 +pairing_bls12381,bn254,groth16,949289,1570543 pairing_bls12381,bls12_377,groth16,0,0 pairing_bls12381,bls12_381,groth16,0,0 pairing_bls12381,bls24_315,groth16,0,0 pairing_bls12381,bls24_317,groth16,0,0 pairing_bls12381,bw6_761,groth16,0,0 pairing_bls12381,bw6_633,groth16,0,0 -pairing_bls12381,bn254,plonk,3642638,3233378 +pairing_bls12381,bn254,plonk,3648951,3239243 pairing_bls12381,bls12_377,plonk,0,0 pairing_bls12381,bls12_381,plonk,0,0 pairing_bls12381,bls24_315,plonk,0,0 @@ -181,14 +181,14 @@ pairing_bls24315,bls24_315,plonk,0,0 pairing_bls24315,bls24_317,plonk,0,0 pairing_bls24315,bw6_761,plonk,0,0 pairing_bls24315,bw6_633,plonk,141249,141249 -pairing_bn254,bn254,groth16,607378,995098 +pairing_bn254,bn254,groth16,607322,995002 pairing_bn254,bls12_377,groth16,0,0 pairing_bn254,bls12_381,groth16,0,0 pairing_bn254,bls24_315,groth16,0,0 pairing_bn254,bls24_317,groth16,0,0 pairing_bn254,bw6_761,groth16,0,0 pairing_bn254,bw6_633,groth16,0,0 -pairing_bn254,bn254,plonk,2329131,2039205 +pairing_bn254,bn254,plonk,2328923,2039005 pairing_bn254,bls12_377,plonk,0,0 pairing_bn254,bls12_381,plonk,0,0 pairing_bn254,bls24_315,plonk,0,0 diff --git a/internal/stats/snippet.go b/internal/stats/snippet.go index ccab6700..b63e1eb5 100644 --- a/internal/stats/snippet.go +++ b/internal/stats/snippet.go @@ -91,8 +91,8 @@ func initSnippets() { x13 := secp256k1.Mul(newElement(), newElement()) x13 = secp256k1.Mul(x13, newElement()) - five := emulated.ValueOf[emulated.Secp256k1Fp](5) - fx2 := secp256k1.Mul(&five, newElement()) + five := secp256k1.NewElement(5) + fx2 := secp256k1.Mul(five, newElement()) nom := secp256k1.Sub(fx2, x13) denom := secp256k1.Add(newElement(), newElement()) denom = secp256k1.Add(denom, newElement()) diff --git a/internal/tinyfield/arith.go b/internal/tinyfield/arith.go deleted file mode 100644 index d86ebc99..00000000 --- a/internal/tinyfield/arith.go +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2020-2025 Consensys Software Inc. -// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. - -// Code generated by consensys/gnark-crypto DO NOT EDIT - -package tinyfield - -import ( - "math/bits" -) - -// madd0 hi = a*b + c (discards lo bits) -func madd0(a, b, c uint64) (hi uint64) { - var carry, lo uint64 - hi, lo = bits.Mul64(a, b) - _, carry = bits.Add64(lo, c, 0) - hi, _ = bits.Add64(hi, 0, carry) - return -} - -// madd1 hi, lo = a*b + c -func madd1(a, b, c uint64) (hi uint64, lo uint64) { - var carry uint64 - hi, lo = bits.Mul64(a, b) - lo, carry = bits.Add64(lo, c, 0) - hi, _ = bits.Add64(hi, 0, carry) - return -} - -// madd2 hi, lo = a*b + c + d -func madd2(a, b, c, d uint64) (hi uint64, lo uint64) { - var carry uint64 - hi, lo = bits.Mul64(a, b) - c, carry = bits.Add64(c, d, 0) - hi, _ = bits.Add64(hi, 0, carry) - lo, carry = bits.Add64(lo, c, 0) - hi, _ = bits.Add64(hi, 0, carry) - return -} - -func madd3(a, b, c, d, e uint64) (hi uint64, lo uint64) { - var carry uint64 - hi, lo = bits.Mul64(a, b) - c, carry = bits.Add64(c, d, 0) - hi, _ = bits.Add64(hi, 0, carry) - lo, carry = bits.Add64(lo, c, 0) - hi, _ = bits.Add64(hi, e, carry) - return -} diff --git a/internal/tinyfield/element_purego.go b/internal/tinyfield/element_purego.go deleted file mode 100644 index c21a4bf4..00000000 --- a/internal/tinyfield/element_purego.go +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2020-2025 Consensys Software Inc. -// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. - -// Code generated by consensys/gnark-crypto DO NOT EDIT - -package tinyfield - -import "math/bits" - -// MulBy3 x *= 3 (mod q) -func MulBy3(x *Element) { - var y Element - y.SetUint64(3) - x.Mul(x, &y) -} - -// MulBy5 x *= 5 (mod q) -func MulBy5(x *Element) { - var y Element - y.SetUint64(5) - x.Mul(x, &y) -} - -// MulBy13 x *= 13 (mod q) -func MulBy13(x *Element) { - var y Element - y.SetUint64(13) - x.Mul(x, &y) -} - -func fromMont(z *Element) { - _fromMontGeneric(z) -} - -func reduce(z *Element) { - _reduceGeneric(z) -} - -// Mul z = x * y (mod q) -// -// x and y must be less than q -func (z *Element) Mul(x, y *Element) *Element { - - // In fact, since the modulus R fits on one register, the CIOS algorithm gets reduced to standard REDC (textbook Montgomery reduction): - // hi, lo := x * y - // m := (lo * qInvNeg) mod R - // (*) r := (hi * R + lo + m * q) / R - // reduce r if necessary - - // On the emphasized line, we get r = hi + (lo + m * q) / R - // If we write hi2, lo2 = m * q then R | m * q - lo2 ⇒ R | (lo * qInvNeg) q - lo2 = -lo - lo2 - // This shows lo + lo2 = 0 mod R. i.e. lo + lo2 = 0 if lo = 0 and R otherwise. - // Which finally gives (lo + m * q) / R = (lo + lo2 + R hi2) / R = hi2 + (lo+lo2) / R = hi2 + (lo != 0) - // This "optimization" lets us do away with one MUL instruction on ARM architectures and is available for all q < R. - - hi, lo := bits.Mul64(x[0], y[0]) - if lo != 0 { - hi++ // x[0] * y[0] ≤ 2¹²⁸ - 2⁶⁵ + 1, meaning hi ≤ 2⁶⁴ - 2 so no need to worry about overflow - } - m := lo * qInvNeg - hi2, _ := bits.Mul64(m, q) - r, carry := bits.Add64(hi2, hi, 0) - if carry != 0 || r >= q { - // we need to reduce - r -= q - } - z[0] = r - - return z -} - -// Square z = x * x (mod q) -// -// x must be less than q -func (z *Element) Square(x *Element) *Element { - // see Mul for algorithm documentation - - // In fact, since the modulus R fits on one register, the CIOS algorithm gets reduced to standard REDC (textbook Montgomery reduction): - // hi, lo := x * y - // m := (lo * qInvNeg) mod R - // (*) r := (hi * R + lo + m * q) / R - // reduce r if necessary - - // On the emphasized line, we get r = hi + (lo + m * q) / R - // If we write hi2, lo2 = m * q then R | m * q - lo2 ⇒ R | (lo * qInvNeg) q - lo2 = -lo - lo2 - // This shows lo + lo2 = 0 mod R. i.e. lo + lo2 = 0 if lo = 0 and R otherwise. - // Which finally gives (lo + m * q) / R = (lo + lo2 + R hi2) / R = hi2 + (lo+lo2) / R = hi2 + (lo != 0) - // This "optimization" lets us do away with one MUL instruction on ARM architectures and is available for all q < R. - - hi, lo := bits.Mul64(x[0], x[0]) - if lo != 0 { - hi++ // x[0] * y[0] ≤ 2¹²⁸ - 2⁶⁵ + 1, meaning hi ≤ 2⁶⁴ - 2 so no need to worry about overflow - } - m := lo * qInvNeg - hi2, _ := bits.Mul64(m, q) - r, carry := bits.Add64(hi2, hi, 0) - if carry != 0 || r >= q { - // we need to reduce - r -= q - } - z[0] = r - - return z -} - -// Butterfly sets -// -// a = a + b (mod q) -// b = a - b (mod q) -func Butterfly(a, b *Element) { - _butterflyGeneric(a, b) -} diff --git a/internal/utils/algo_utils_test.go b/internal/utils/algo_utils_test.go index 2925bd66..91ef0df2 100644 --- a/internal/utils/algo_utils_test.go +++ b/internal/utils/algo_utils_test.go @@ -1,8 +1,9 @@ package utils import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func SliceLen[T any](slice []T) int { diff --git a/internal/utils/convert.go b/internal/utils/convert.go index 083d620a..5d949596 100644 --- a/internal/utils/convert.go +++ b/internal/utils/convert.go @@ -105,3 +105,18 @@ func Uint64SliceSliceToIntSliceSlice(in [][]uint64) [][]int { } return res } + +// ForceUint32 converts an object that may have been a uint64, or a uint32, to a uint32. +func ForceUint32(v any) uint32 { + switch x := v.(type) { + case uint32: + return x + case uint64: + if x > 0xFFFFFFFF { + panic("value too large to fit in uint32") + } + return uint32(x) + default: + panic("value is not uint32 or uint64") + } +} diff --git a/internal/utils/parallelize.go b/internal/utils/parallelize.go index bc377ccc..88cdc7fa 100644 --- a/internal/utils/parallelize.go +++ b/internal/utils/parallelize.go @@ -1,12 +1,19 @@ package utils import ( + "os" "runtime" "sync" ) // Parallelize process in parallel the work function func Parallelize(nbIterations int, work func(int, int), maxCpus ...int) { + if os.Getenv("DISABLE_GOROUTINE") == "1" { + for i := 0; i < nbIterations; i++ { + work(i, i+1) + } + return + } nbTasks := runtime.NumCPU() if len(maxCpus) == 1 { diff --git a/internal/utils/slices.go b/internal/utils/slices.go index 9c50ed9b..dd2e2db3 100644 --- a/internal/utils/slices.go +++ b/internal/utils/slices.go @@ -7,3 +7,12 @@ func AppendRefs[T any](s []any, v []T) []any { } return s } + +// References returns a slice of references to the elements of v. +func References[T any](v []T) []*T { + res := make([]*T, len(v)) + for i := range v { + res[i] = &v[i] + } + return res +} diff --git a/internal/utils/test_utils/test_utils.go b/internal/utils/test_utils/test_utils.go index 51f0a9d0..84f49dcc 100644 --- a/internal/utils/test_utils/test_utils.go +++ b/internal/utils/test_utils/test_utils.go @@ -2,9 +2,10 @@ package test_utils import ( "bytes" - "github.com/stretchr/testify/require" "io" "testing" + + "github.com/stretchr/testify/require" ) // Range (n, startingPoints...) = [startingPoints[0], startingPoints[0]+1, ..., startingPoints[0]+n-1, startingPoints[1], startingPoints[1]+1, ...,] diff --git a/internal/widecommitter/widecommitter.go b/internal/widecommitter/widecommitter.go new file mode 100644 index 00000000..59ce0557 --- /dev/null +++ b/internal/widecommitter/widecommitter.go @@ -0,0 +1,99 @@ +// package widecommitter provides mocked implementation of the widecommitter interface +package widecommitter + +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/kvstore" + "github.com/consensys/gnark/logger" + "golang.org/x/crypto/sha3" +) + +// wrappedBuilder mimics the behaviour of the backend which has wide commitment +// and rangechecker capabilities. We don't have these in gnark yet, but we +// in principle allow other backends to implement them. +type wrappedBuilder struct { + requiredBuilderInterface +} + +type requiredBuilderInterface interface { + frontend.Builder[constraint.U32] + kvstore.Store +} + +// From creates a new builder that implements [frontend.WideCommitter] and [frontend.Rangechecker]. +// It wraps the given builder and implements the required methods. This is useful for testing +// circuit solvability and compilation without needing to implement the full functionality of the backend. +// +// NB! The [Check] method is a no-op and does not perform any checks. The [WideCommit] method does not +// perform any checks in the proof system, returning pseudo-random values instead. +func From(newBuilder frontend.NewBuilderU32) frontend.NewBuilderU32 { + return func(field *big.Int, config frontend.CompileConfig) (frontend.Builder[constraint.U32], error) { + b, err := newBuilder(field, config) + if err != nil { + return nil, err + } + bb, ok := b.(requiredBuilderInterface) + if !ok { + return nil, fmt.Errorf("builder does not implement required interface") + } + log := logger.Logger() + log.Warn().Msg("using fake wide committer, no checks will be performed. Use only for testing") + return &wrappedBuilder{bb}, nil + } +} + +func (w *wrappedBuilder) WideCommit(width int, toCommit ...frontend.Variable) (commitment []frontend.Variable, err error) { + res, err := w.NewHint(mockedWideCommitHint, width, toCommit...) + return res, err +} + +func (w *wrappedBuilder) Compiler() frontend.Compiler { + return w +} + +func (w *wrappedBuilder) Check(in frontend.Variable, width int) { + _, err := w.NewHint(mockedRangecheckHint, 1, width, in) + if err != nil { + panic(fmt.Sprintf("failed to check range: %v", err)) + } +} + +func mockedWideCommitHint(m *big.Int, inputs []*big.Int, outputs []*big.Int) error { + nb := (m.BitLen() + 7) / 8 + buf := make([]byte, nb) + hasher := sha3.NewCShake128(nil, []byte("gnark test engine")) + for _, in := range inputs { + bs := in.FillBytes(buf) + hasher.Write(bs) + } + for i := range len(outputs) { + hasher.Read(buf) + outputs[i].SetBytes(buf) + outputs[i].Mod(outputs[i], m) + } + return nil +} + +func mockedRangecheckHint(m *big.Int, inputs []*big.Int, outputs []*big.Int) error { + // this mocked range check hint errors in case the input is not in the range. + // it doesn't enforce in the proof system, but it is useful for testing and checking + // consistency with the test engine + if len(inputs) != 2 { + return fmt.Errorf("expected 2 inputs, got %d", len(inputs)) + } + width := inputs[0].Int64() + val := inputs[1] + if val.BitLen() > int(width) { + return fmt.Errorf("value %s is not less than %d bits", val, width) + } + return nil +} + +func init() { + solver.RegisterHint(mockedWideCommitHint, mockedRangecheckHint) +} diff --git a/std/algebra/emulated/fields_bls12381/e2.go b/std/algebra/emulated/fields_bls12381/e2.go index ba378f67..43380947 100644 --- a/std/algebra/emulated/fields_bls12381/e2.go +++ b/std/algebra/emulated/fields_bls12381/e2.go @@ -48,7 +48,7 @@ func NewExt2(api frontend.API) *Ext2 { nonResidues := make(map[int]map[int]*E2) for pwr, v := range pwrs { for coeff, v := range v { - el := E2{emulated.ValueOf[emulated.BLS12381Fp](v.A0), emulated.ValueOf[emulated.BLS12381Fp](v.A1)} + el := E2{*fp.NewElement(v.A0), *fp.NewElement(v.A1)} if nonResidues[pwr] == nil { nonResidues[pwr] = make(map[int]*E2) } @@ -109,10 +109,10 @@ func (e Ext2) MulByNonResidue1Power1(x *E2) *E2 { // MulByNonResidue1Power2 returns x*(1+u)^(2*(p^1-1)/6) func (e Ext2) MulByNonResidue1Power2(x *E2) *E2 { - element := emulated.ValueOf[emulated.BLS12381Fp]("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939436") - a := e.fp.Mul(&x.A1, &element) + element := e.fp.NewElement("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939436") + a := e.fp.Mul(&x.A1, element) a = e.fp.Neg(a) - b := e.fp.Mul(&x.A0, &element) + b := e.fp.Mul(&x.A0, element) return &E2{ A0: *a, A1: *b, @@ -126,10 +126,10 @@ func (e Ext2) MulByNonResidue1Power3(x *E2) *E2 { // MulByNonResidue1Power4 returns x*(1+u)^(4*(p^1-1)/6) func (e Ext2) MulByNonResidue1Power4(x *E2) *E2 { - element := emulated.ValueOf[emulated.BLS12381Fp]("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939437") + element := e.fp.NewElement("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939437") return &E2{ - A0: *e.fp.Mul(&x.A0, &element), - A1: *e.fp.Mul(&x.A1, &element), + A0: *e.fp.Mul(&x.A0, element), + A1: *e.fp.Mul(&x.A1, element), } } @@ -140,46 +140,46 @@ func (e Ext2) MulByNonResidue1Power5(x *E2) *E2 { // MulByNonResidue2Power1 returns x*(1+u)^(1*(p^2-1)/6) func (e Ext2) MulByNonResidue2Power1(x *E2) *E2 { - element := emulated.ValueOf[emulated.BLS12381Fp]("793479390729215512621379701633421447060886740281060493010456487427281649075476305620758731620351") + element := e.fp.NewElement("793479390729215512621379701633421447060886740281060493010456487427281649075476305620758731620351") return &E2{ - A0: *e.fp.Mul(&x.A0, &element), - A1: *e.fp.Mul(&x.A1, &element), + A0: *e.fp.Mul(&x.A0, element), + A1: *e.fp.Mul(&x.A1, element), } } // MulByNonResidue2Power2 returns x*(1+u)^(2*(p^2-1)/6) func (e Ext2) MulByNonResidue2Power2(x *E2) *E2 { - element := emulated.ValueOf[emulated.BLS12381Fp]("793479390729215512621379701633421447060886740281060493010456487427281649075476305620758731620350") + element := e.fp.NewElement("793479390729215512621379701633421447060886740281060493010456487427281649075476305620758731620350") return &E2{ - A0: *e.fp.Mul(&x.A0, &element), - A1: *e.fp.Mul(&x.A1, &element), + A0: *e.fp.Mul(&x.A0, element), + A1: *e.fp.Mul(&x.A1, element), } } // MulByNonResidue2Power3 returns x*(1+u)^(3*(p^2-1)/6) func (e Ext2) MulByNonResidue2Power3(x *E2) *E2 { - element := emulated.ValueOf[emulated.BLS12381Fp]("4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559786") + element := e.fp.NewElement("4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559786") return &E2{ - A0: *e.fp.Mul(&x.A0, &element), - A1: *e.fp.Mul(&x.A1, &element), + A0: *e.fp.Mul(&x.A0, element), + A1: *e.fp.Mul(&x.A1, element), } } // MulByNonResidue2Power4 returns x*(1+u)^(4*(p^2-1)/6) func (e Ext2) MulByNonResidue2Power4(x *E2) *E2 { - element := emulated.ValueOf[emulated.BLS12381Fp]("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939436") + element := e.fp.NewElement("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939436") return &E2{ - A0: *e.fp.Mul(&x.A0, &element), - A1: *e.fp.Mul(&x.A1, &element), + A0: *e.fp.Mul(&x.A0, element), + A1: *e.fp.Mul(&x.A1, element), } } // MulByNonResidue2Power5 returns x*(1+u)^(5*(p^2-1)/6) func (e Ext2) MulByNonResidue2Power5(x *E2) *E2 { - element := emulated.ValueOf[emulated.BLS12381Fp]("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939437") + element := e.fp.NewElement("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939437") return &E2{ - A0: *e.fp.Mul(&x.A0, &element), - A1: *e.fp.Mul(&x.A1, &element), + A0: *e.fp.Mul(&x.A0, element), + A1: *e.fp.Mul(&x.A1, element), } } @@ -292,6 +292,14 @@ func (e Ext2) AssertIsEqual(x, y *E2) { e.fp.AssertIsEqual(&x.A1, &y.A1) } +func (e Ext2) IsEqual(x, y *E2) frontend.Variable { + xDiff := e.fp.Sub(&x.A0, &y.A0) + yDiff := e.fp.Sub(&x.A1, &y.A1) + xIsZero := e.fp.IsZero(xDiff) + yIsZero := e.fp.IsZero(yDiff) + return e.api.And(xIsZero, yIsZero) +} + func FromE2(y *bls12381.E2) E2 { return E2{ A0: emulated.ValueOf[emulated.BLS12381Fp](y.A0), diff --git a/std/algebra/emulated/fields_bn254/e2.go b/std/algebra/emulated/fields_bn254/e2.go index 7af33cbd..b065293d 100644 --- a/std/algebra/emulated/fields_bn254/e2.go +++ b/std/algebra/emulated/fields_bn254/e2.go @@ -49,7 +49,7 @@ func NewExt2(api frontend.API) *Ext2 { nonResidues := make(map[int]map[int]*E2) for pwr, v := range pwrs { for coeff, v := range v { - el := E2{emulated.ValueOf[emulated.BN254Fp](v.A0), emulated.ValueOf[emulated.BN254Fp](v.A1)} + el := E2{*fp.NewElement(v.A0), *fp.NewElement(v.A1)} if nonResidues[pwr] == nil { nonResidues[pwr] = make(map[int]*E2) } @@ -132,46 +132,46 @@ func (e Ext2) MulByNonResidue1Power5(x *E2) *E2 { // MulByNonResidue2Power1 returns x*(9+u)^(1*(p^2-1)/6) func (e Ext2) MulByNonResidue2Power1(x *E2) *E2 { - element := emulated.ValueOf[emulated.BN254Fp]("21888242871839275220042445260109153167277707414472061641714758635765020556617") + element := e.fp.NewElement("21888242871839275220042445260109153167277707414472061641714758635765020556617") return &E2{ - A0: *e.fp.Mul(&x.A0, &element), - A1: *e.fp.Mul(&x.A1, &element), + A0: *e.fp.Mul(&x.A0, element), + A1: *e.fp.Mul(&x.A1, element), } } // MulByNonResidue2Power2 returns x*(9+u)^(2*(p^2-1)/6) func (e Ext2) MulByNonResidue2Power2(x *E2) *E2 { - element := emulated.ValueOf[emulated.BN254Fp]("21888242871839275220042445260109153167277707414472061641714758635765020556616") + element := e.fp.NewElement("21888242871839275220042445260109153167277707414472061641714758635765020556616") return &E2{ - A0: *e.fp.Mul(&x.A0, &element), - A1: *e.fp.Mul(&x.A1, &element), + A0: *e.fp.Mul(&x.A0, element), + A1: *e.fp.Mul(&x.A1, element), } } // MulByNonResidue2Power3 returns x*(9+u)^(3*(p^2-1)/6) func (e Ext2) MulByNonResidue2Power3(x *E2) *E2 { - element := emulated.ValueOf[emulated.BN254Fp]("21888242871839275222246405745257275088696311157297823662689037894645226208582") + element := e.fp.NewElement("21888242871839275222246405745257275088696311157297823662689037894645226208582") return &E2{ - A0: *e.fp.Mul(&x.A0, &element), - A1: *e.fp.Mul(&x.A1, &element), + A0: *e.fp.Mul(&x.A0, element), + A1: *e.fp.Mul(&x.A1, element), } } // MulByNonResidue2Power4 returns x*(9+u)^(4*(p^2-1)/6) func (e Ext2) MulByNonResidue2Power4(x *E2) *E2 { - element := emulated.ValueOf[emulated.BN254Fp]("2203960485148121921418603742825762020974279258880205651966") + element := e.fp.NewElement("2203960485148121921418603742825762020974279258880205651966") return &E2{ - A0: *e.fp.Mul(&x.A0, &element), - A1: *e.fp.Mul(&x.A1, &element), + A0: *e.fp.Mul(&x.A0, element), + A1: *e.fp.Mul(&x.A1, element), } } // MulByNonResidue2Power5 returns x*(9+u)^(5*(p^2-1)/6) func (e Ext2) MulByNonResidue2Power5(x *E2) *E2 { - element := emulated.ValueOf[emulated.BN254Fp]("2203960485148121921418603742825762020974279258880205651967") + element := e.fp.NewElement("2203960485148121921418603742825762020974279258880205651967") return &E2{ - A0: *e.fp.Mul(&x.A0, &element), - A1: *e.fp.Mul(&x.A1, &element), + A0: *e.fp.Mul(&x.A0, element), + A1: *e.fp.Mul(&x.A1, element), } } diff --git a/std/algebra/emulated/fields_bw6761/e6.go b/std/algebra/emulated/fields_bw6761/e6.go index 125c902d..f906c0de 100644 --- a/std/algebra/emulated/fields_bw6761/e6.go +++ b/std/algebra/emulated/fields_bw6761/e6.go @@ -1109,6 +1109,26 @@ func (e Ext6) AssertIsEqual(a, b *E6) { } +func (e Ext6) IsEqual(x, y *E6) frontend.Variable { + diff0 := e.fp.Sub(&x.A0, &y.A0) + diff1 := e.fp.Sub(&x.A1, &y.A1) + diff2 := e.fp.Sub(&x.A2, &y.A2) + diff3 := e.fp.Sub(&x.A3, &y.A3) + diff4 := e.fp.Sub(&x.A4, &y.A4) + diff5 := e.fp.Sub(&x.A5, &y.A5) + isZero0 := e.fp.IsZero(diff0) + isZero1 := e.fp.IsZero(diff1) + isZero2 := e.fp.IsZero(diff2) + isZero3 := e.fp.IsZero(diff3) + isZero4 := e.fp.IsZero(diff4) + isZero5 := e.fp.IsZero(diff5) + + return e.api.And( + e.api.And(e.api.And(isZero0, isZero1), e.api.And(isZero2, isZero3)), + e.api.And(isZero4, isZero5), + ) +} + func (e Ext6) Copy(x *E6) *E6 { return &E6{ A0: x.A0, @@ -1148,17 +1168,17 @@ func (e Ext6) Select(selector frontend.Variable, z1, z0 *E6) *E6 { // Frobenius set z in E6 to Frobenius(x), return z func (e Ext6) Frobenius(x *E6) *E6 { - _frobA := emulated.ValueOf[emulated.BW6761Fp]("4922464560225523242118178942575080391082002530232324381063048548642823052024664478336818169867474395270858391911405337707247735739826664939444490469542109391530482826728203582549674992333383150446779312029624171857054392282775648") - _frobB := emulated.ValueOf[emulated.BW6761Fp]("1968985824090209297278610739700577151397666382303825728450741611566800370218827257750865013421937292370006175842381275743914023380727582819905021229583192207421122272650305267822868639090213645505120388400344940985710520836292650") - _frobC := emulated.ValueOf[emulated.BW6761Fp]("4922464560225523242118178942575080391082002530232324381063048548642823052024664478336818169867474395270858391911405337707247735739826664939444490469542109391530482826728203582549674992333383150446779312029624171857054392282775649") - _frobBC := emulated.ValueOf[emulated.BW6761Fp]("1968985824090209297278610739700577151397666382303825728450741611566800370218827257750865013421937292370006175842381275743914023380727582819905021229583192207421122272650305267822868639090213645505120388400344940985710520836292651") + _frobA := e.fp.NewElement("4922464560225523242118178942575080391082002530232324381063048548642823052024664478336818169867474395270858391911405337707247735739826664939444490469542109391530482826728203582549674992333383150446779312029624171857054392282775648") + _frobB := e.fp.NewElement("1968985824090209297278610739700577151397666382303825728450741611566800370218827257750865013421937292370006175842381275743914023380727582819905021229583192207421122272650305267822868639090213645505120388400344940985710520836292650") + _frobC := e.fp.NewElement("4922464560225523242118178942575080391082002530232324381063048548642823052024664478336818169867474395270858391911405337707247735739826664939444490469542109391530482826728203582549674992333383150446779312029624171857054392282775649") + _frobBC := e.fp.NewElement("1968985824090209297278610739700577151397666382303825728450741611566800370218827257750865013421937292370006175842381275743914023380727582819905021229583192207421122272650305267822868639090213645505120388400344940985710520836292651") var z E6 z.A0 = x.A0 - z.A2 = *e.fp.Mul(&x.A2, &_frobA) - z.A4 = *e.fp.Mul(&x.A4, &_frobB) - z.A1 = *e.fp.Mul(&x.A1, &_frobC) + z.A2 = *e.fp.Mul(&x.A2, _frobA) + z.A4 = *e.fp.Mul(&x.A4, _frobB) + z.A1 = *e.fp.Mul(&x.A1, _frobC) z.A3 = *e.fp.Neg(&x.A3) - z.A5 = *e.fp.Mul(&x.A5, &_frobBC) + z.A5 = *e.fp.Mul(&x.A5, _frobBC) return &z } diff --git a/std/algebra/emulated/fields_bw6761/e6_pairing.go b/std/algebra/emulated/fields_bw6761/e6_pairing.go index 94e00e4f..5dd2c872 100644 --- a/std/algebra/emulated/fields_bw6761/e6_pairing.go +++ b/std/algebra/emulated/fields_bw6761/e6_pairing.go @@ -2,8 +2,6 @@ package fields_bw6761 import ( "math/big" - - "github.com/consensys/gnark/std/math/emulated" ) func (e Ext6) nSquareKarabina12345(z *E6, n int) *E6 { @@ -278,8 +276,8 @@ func (e Ext6) mul023By023(d0, d1, c0, c1 *baseEl) [5]*baseEl { x01 = e.fp.Sub(x01, tmp) x14 := e.fp.Add(c1, d1) - minusFour := emulated.ValueOf[emulated.BW6761Fp]("6891450384315732539396789682275657542479668912536150109513790160209623422243491736087683183289411687640864567753786613451161759120554247759349511699125301598951605099378508850372543631423596795951899700429969112842764913119068295") // -4 % p - zC0B0 := e.fp.Add(x0, &minusFour) + minusFour := e.fp.NewElement("6891450384315732539396789682275657542479668912536150109513790160209623422243491736087683183289411687640864567753786613451161759120554247759349511699125301598951605099378508850372543631423596795951899700429969112842764913119068295") // -4 % p + zC0B0 := e.fp.Add(x0, minusFour) return [5]*baseEl{zC0B0, x01, x04, x1, x14} } diff --git a/std/algebra/emulated/sw_bls12381/g1.go b/std/algebra/emulated/sw_bls12381/g1.go index ce839861..84af3ec6 100644 --- a/std/algebra/emulated/sw_bls12381/g1.go +++ b/std/algebra/emulated/sw_bls12381/g1.go @@ -28,6 +28,7 @@ func NewG1Affine(v bls12381.G1Affine) G1Affine { } type G1 struct { + api frontend.API curveF *emulated.Field[BaseField] w *emulated.Element[BaseField] } @@ -37,13 +38,23 @@ func NewG1(api frontend.API) (*G1, error) { if err != nil { return nil, fmt.Errorf("new base api: %w", err) } - w := emulated.ValueOf[BaseField]("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939436") + w := ba.NewElement("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939436") return &G1{ + api: api, curveF: ba, - w: &w, + w: w, }, nil } +func (g1 G1) neg(p *G1Affine) *G1Affine { + xr := &p.X + yr := g1.curveF.Neg(&p.Y) + return &G1Affine{ + X: *xr, + Y: *yr, + } +} + func (g1 *G1) phi(q *G1Affine) *G1Affine { x := g1.curveF.Mul(&q.X, g1.w) @@ -157,6 +168,53 @@ func (g1 *G1) scalarMulBySeedSquare(q *G1Affine) *G1Affine { return z } +func (g1 *G1) computeCurveEquation(P *G1Affine) (left, right *baseEl) { + // Curve: Y² == X³ + aX + b, where a=0 and b=4 + // (X,Y) ∈ {Y² == X³ + aX + b} U (0,0) + + // if P=(0,0) we assign b=0 otherwise 4, and continue + selector := g1.api.And(g1.curveF.IsZero(&P.X), g1.curveF.IsZero(&P.Y)) + four := g1.curveF.NewElement("4") + b := g1.curveF.Select(selector, g1.curveF.Zero(), four) + + left = g1.curveF.Mul(&P.Y, &P.Y) + right = g1.curveF.Eval([][]*emulated.Element[BaseField]{{&P.X, &P.X, &P.X}, {b}}, []int{1, 1}) + return left, right +} + +func (g1 *G1) AssertIsOnCurve(P *G1Affine) { + left, right := g1.computeCurveEquation(P) + g1.curveF.AssertIsEqual(left, right) +} + +func (g1 *G1) AssertIsOnG1(P *G1Affine) { + // 1- Check P is on the curve + g1.AssertIsOnCurve(P) + + // 2- Check P has the right subgroup order + // [x²]ϕ(P) + phiP := g1.phi(P) + _P := g1.scalarMulBySeedSquare(phiP) + _P = g1.neg(_P) + + // [r]Q == 0 <==> P = -[x²]ϕ(P) + g1.AssertIsEqual(_P, P) +} + +// AssertIsEqual asserts that p and q are the same point. +func (g1 *G1) AssertIsEqual(p, q *G1Affine) { + g1.curveF.AssertIsEqual(&p.X, &q.X) + g1.curveF.AssertIsEqual(&p.Y, &q.Y) +} + +func (g1 *G1) IsEqual(p, q *G1Affine) frontend.Variable { + xDiff := g1.curveF.Sub(&p.X, &q.X) + yDiff := g1.curveF.Sub(&p.Y, &q.Y) + xIsZero := g1.curveF.IsZero(xDiff) + yIsZero := g1.curveF.IsZero(yDiff) + return g1.api.And(xIsZero, yIsZero) +} + // NewScalar allocates a witness from the native scalar and returns it. func NewScalar(v fr_bls12381.Element) Scalar { return emulated.ValueOf[ScalarField](v) diff --git a/std/algebra/emulated/sw_bls12381/g2.go b/std/algebra/emulated/sw_bls12381/g2.go index 4f3245ee..3b666e85 100644 --- a/std/algebra/emulated/sw_bls12381/g2.go +++ b/std/algebra/emulated/sw_bls12381/g2.go @@ -1,19 +1,29 @@ package sw_bls12381 import ( + "fmt" "math/big" bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" + "github.com/consensys/gnark-crypto/ecc/bls12-381/hash_to_curve" "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/algopts" "github.com/consensys/gnark/std/algebra/emulated/fields_bls12381" "github.com/consensys/gnark/std/math/emulated" ) type G2 struct { - fp *emulated.Field[BaseField] + api frontend.API + fp *emulated.Field[BaseField] + fr *emulated.Field[ScalarField] *fields_bls12381.Ext2 - u1, w *emulated.Element[BaseField] - v *fields_bls12381.E2 + u1, w, w2 *emulated.Element[BaseField] + eigenvalue *emulated.Element[ScalarField] + v *fields_bls12381.E2 + + // SSWU map coefficients + sswuCoeffA, sswuCoeffB *fields_bls12381.E2 + sswuZ *fields_bls12381.E2 } type g2AffP struct { @@ -39,25 +49,52 @@ func newG2AffP(v bls12381.G2Affine) g2AffP { } } -func NewG2(api frontend.API) *G2 { - fp, err := emulated.NewField[emulated.BLS12381Fp](api) +func NewG2(api frontend.API) (*G2, error) { + fp, err := emulated.NewField[BaseField](api) if err != nil { - // TODO: we start returning errors when generifying - panic(err) + return nil, fmt.Errorf("new base api: %w", err) + } + fr, err := emulated.NewField[ScalarField](api) + if err != nil { + return nil, fmt.Errorf("new scalar api: %w", err) } - w := emulated.ValueOf[BaseField]("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939436") - u1 := emulated.ValueOf[BaseField]("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939437") + w := fp.NewElement("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939436") + w2 := fp.NewElement("793479390729215512621379701633421447060886740281060493010456487427281649075476305620758731620350") + eigenvalue := fr.NewElement("228988810152649578064853576960394133503") + u1 := fp.NewElement("4002409555221667392624310435006688643935503118305586438271171395842971157480381377015405980053539358417135540939437") v := fields_bls12381.E2{ - A0: emulated.ValueOf[BaseField]("2973677408986561043442465346520108879172042883009249989176415018091420807192182638567116318576472649347015917690530"), - A1: emulated.ValueOf[BaseField]("1028732146235106349975324479215795277384839936929757896155643118032610843298655225875571310552543014690878354869257"), + A0: *fp.NewElement("2973677408986561043442465346520108879172042883009249989176415018091420807192182638567116318576472649347015917690530"), + A1: *fp.NewElement("1028732146235106349975324479215795277384839936929757896155643118032610843298655225875571310552543014690878354869257"), } - return &G2{ - fp: fp, - Ext2: fields_bls12381.NewExt2(api), - w: &w, - u1: &u1, - v: &v, + sswuCoeffA, sswuCoeffB := hash_to_curve.G2SSWUIsogenyCurveCoefficients() + coeffA := &fields_bls12381.E2{ + A0: *fp.NewElement(sswuCoeffA.A0), + A1: *fp.NewElement(sswuCoeffA.A1), + } + coeffB := &fields_bls12381.E2{ + A0: *fp.NewElement(sswuCoeffB.A0), + A1: *fp.NewElement(sswuCoeffB.A1), } + sswuZ := hash_to_curve.G2SSWUIsogenyZ() + z := &fields_bls12381.E2{ + A0: *fp.NewElement(sswuZ.A0), + A1: *fp.NewElement(sswuZ.A1), + } + return &G2{ + api: api, + fp: fp, + fr: fr, + Ext2: fields_bls12381.NewExt2(api), + w: w, + w2: w2, + eigenvalue: eigenvalue, + u1: u1, + v: &v, + // SSWU map + sswuCoeffA: coeffA, + sswuCoeffB: coeffB, + sswuZ: z, + }, nil } func NewG2Affine(v bls12381.G2Affine) G2Affine { @@ -69,6 +106,15 @@ func NewG2Affine(v bls12381.G2Affine) G2Affine { // NewG2AffineFixed returns witness of v with precomputations for efficient // pairing computation. func NewG2AffineFixed(v bls12381.G2Affine) G2Affine { + if !v.IsInSubGroup() { + // for the pairing check we check that G2 point is already in the + // subgroup when we compute the lines in circuit. However, when the + // point is given as a constant, then we already precompute the lines at + // circuit compile time without explicitly checking the G2 membership. + // So, we need to check that the point is in the subgroup before we + // compute the lines. + panic("given point is not in the G2 subgroup") + } lines := precomputeLines(v) return G2Affine{ P: newG2AffP(v), @@ -103,6 +149,18 @@ func (g2 *G2) psi(q *G2Affine) *G2Affine { } } +func (g2 *G2) psi2(q *G2Affine) *G2Affine { + x := g2.Ext2.MulByElement(&q.P.X, g2.w) + y := g2.Ext2.Neg(&q.P.Y) + + return &G2Affine{ + P: g2AffP{ + X: *x, + Y: *y, + }, + } +} + func (g2 *G2) scalarMulBySeed(q *G2Affine) *G2Affine { z := g2.triple(q) @@ -119,6 +177,61 @@ func (g2 *G2) scalarMulBySeed(q *G2Affine) *G2Affine { return g2.neg(z) } +// AddUnified adds p and q and returns it. It doesn't modify p nor q. +// +// ✅ p can be equal to q, and either or both can be (0,0). +// ([0,0],[0,0]) is not on the twist but we conventionally take it as the +// neutral/infinity point as per the [EVM]. +// +// It uses the unified formulas of Brier and Joye ([[BriJoy02]] (Corollary 1)). +// +// [BriJoy02]: https://link.springer.com/content/pdf/10.1007/3-540-45664-3_24.pdf +// [EVM]: https://ethereum.github.io/yellowpaper/paper.pdf +func (g2 *G2) AddUnified(p, q *G2Affine) *G2Affine { + + // selector1 = 1 when p is ([0,0],[0,0]) and 0 otherwise + selector1 := g2.api.And(g2.Ext2.IsZero(&p.P.X), g2.Ext2.IsZero(&p.P.Y)) + // selector2 = 1 when q is ([0,0],[0,0]) and 0 otherwise + selector2 := g2.api.And(g2.Ext2.IsZero(&q.P.X), g2.Ext2.IsZero(&q.P.Y)) + // λ = ((p.x+q.x)² - p.x*q.x + a)/(p.y + q.y) + pxqx := g2.Mul(&p.P.X, &q.P.X) + pxplusqx := g2.Add(&p.P.X, &q.P.X) + num := g2.Mul(pxplusqx, pxplusqx) + num = g2.Sub(num, pxqx) + denum := g2.Add(&p.P.Y, &q.P.Y) + // if p.y + q.y = 0, assign dummy 1 to denum and continue + selector3 := g2.IsZero(denum) + denum = g2.Ext2.Select(selector3, g2.One(), denum) + λ := g2.DivUnchecked(num, denum) + + // x = λ^2 - p.x - q.x + xr := g2.Mul(λ, λ) + xr = g2.Sub(xr, pxplusqx) + + // y = λ(p.x - xr) - p.y + yr := g2.Sub(&p.P.X, xr) + yr = g2.Mul(yr, λ) + yr = g2.Sub(yr, &p.P.Y) + result := &G2Affine{ + P: g2AffP{X: *xr, Y: *yr}, + Lines: nil, + } + + zero := g2.Ext2.Zero() + infinity := G2Affine{ + P: g2AffP{X: *zero, Y: *zero}, + Lines: nil, + } + // if p=([0,0],[0,0]) return q + result = g2.Select(selector1, q, result) + // if q=([0,0],[0,0]) return p + result = g2.Select(selector2, p, result) + // if p.y + q.y = 0, return ([0,0],[0,0]) + result = g2.Select(selector3, &infinity, result) + + return result +} + func (g2 G2) add(p, q *G2Affine) *G2Affine { mone := g2.fp.NewElement(-1) @@ -276,8 +389,444 @@ func (g2 G2) doubleAndAdd(p, q *G2Affine) *G2Affine { } } +// doubleAndAddSelect is the same as doubleAndAdd but computes either: +// +// 2p+q if b=1 or +// 2q+p if b=0 +// +// It first computes the x-coordinate of p+q via the slope(p,q) +// and then based on a Select adds either p or q. +func (g2 G2) doubleAndAddSelect(b frontend.Variable, p, q *G2Affine) *G2Affine { + mone := g2.fp.NewElement(-1) + + // compute λ1 = (q.y-p.y)/(q.x-p.x) + yqyp := g2.Ext2.Sub(&q.P.Y, &p.P.Y) + xqxp := g2.Ext2.Sub(&q.P.X, &p.P.X) + λ1 := g2.Ext2.DivUnchecked(yqyp, xqxp) + + // compute x2 = λ1²-p.x-q.x + x20 := g2.fp.Eval([][]*baseEl{{&λ1.A0, &λ1.A0}, {mone, &λ1.A1, &λ1.A1}, {mone, &p.P.X.A0}, {mone, &q.P.X.A0}}, []int{1, 1, 1, 1}) + x21 := g2.fp.Eval([][]*baseEl{{&λ1.A0, &λ1.A1}, {mone, &p.P.X.A1}, {mone, &q.P.X.A1}}, []int{2, 1, 1}) + x2 := &fields_bls12381.E2{A0: *x20, A1: *x21} + + // omit y2 computation + + // conditional second addition + t := g2.Select(b, p, q) + + // compute -λ2 = λ1+2*t.y/(x2-t.x) + ypyp := g2.Ext2.Add(&t.P.Y, &t.P.Y) + x2xp := g2.Ext2.Sub(x2, &t.P.X) + λ2 := g2.Ext2.DivUnchecked(ypyp, x2xp) + λ2 = g2.Ext2.Add(λ1, λ2) + + // compute x3 = (-λ2)²-t.x-x2 + x30 := g2.fp.Eval([][]*baseEl{{&λ2.A0, &λ2.A0}, {mone, &λ2.A1, &λ2.A1}, {mone, &t.P.X.A0}, {mone, x20}}, []int{1, 1, 1, 1}) + x31 := g2.fp.Eval([][]*baseEl{{&λ2.A0, &λ2.A1}, {mone, &t.P.X.A1}, {mone, x21}}, []int{2, 1, 1}) + x3 := &fields_bls12381.E2{A0: *x30, A1: *x31} + + // compute y3 = -λ2*(x3 - t.x)-t.y + y3 := g2.Ext2.Sub(x3, &t.P.X) + y30 := g2.fp.Eval([][]*baseEl{{&λ2.A0, &y3.A0}, {mone, &λ2.A1, &y3.A1}, {mone, &t.P.Y.A0}}, []int{1, 1, 1}) + y31 := g2.fp.Eval([][]*baseEl{{&λ2.A0, &y3.A1}, {&λ2.A1, &y3.A0}, {mone, &t.P.Y.A1}}, []int{1, 1, 1}) + y3 = &fields_bls12381.E2{A0: *y30, A1: *y31} + + return &G2Affine{ + P: g2AffP{ + X: *x3, + Y: *y3, + }, + } +} + +func (g2 *G2) computeTwistEquation(Q *G2Affine) (left, right *fields_bls12381.E2) { + // Twist: Y² == X³ + aX + b, where a=0 and b=4(1+u) + // (X,Y) ∈ {Y² == X³ + aX + b} U (0,0) + bTwist := fields_bls12381.E2{ + A0: *g2.fp.NewElement("4"), + A1: *g2.fp.NewElement("4"), + } + // if Q=(0,0) we assign b=0 otherwise 4(1+u), and continue + selector := g2.api.And(g2.Ext2.IsZero(&Q.P.X), g2.Ext2.IsZero(&Q.P.Y)) + b := g2.Ext2.Select(selector, g2.Ext2.Zero(), &bTwist) + + left = g2.Ext2.Square(&Q.P.Y) + mone := g2.fp.NewElement(-1) + right = &fields_bls12381.E2{ + A0: *g2.fp.Eval([][]*baseEl{{&Q.P.X.A0, &Q.P.X.A0, &Q.P.X.A0}, {mone, &Q.P.X.A0, &Q.P.X.A1, &Q.P.X.A1}, {&b.A0}}, []int{1, 3, 1}), + A1: *g2.fp.Eval([][]*baseEl{{&Q.P.X.A1, &Q.P.X.A0, &Q.P.X.A0}, {mone, &Q.P.X.A1, &Q.P.X.A1, &Q.P.X.A1}, {&b.A1}}, []int{3, 1, 1}), + } + + return left, right +} + +func (g2 *G2) AssertIsOnTwist(Q *G2Affine) { + left, right := g2.computeTwistEquation(Q) + g2.Ext2.AssertIsEqual(left, right) +} + +func (g2 *G2) AssertIsOnG2(Q *G2Affine) { + // 1- Check Q is on the curve + g2.AssertIsOnTwist(Q) + + // 2- Check Q has the right subgroup order + // [x₀]Q + xQ := g2.scalarMulBySeed(Q) + // ψ(Q) + psiQ := g2.psi(Q) + + // [r]Q == 0 <==> ψ(Q) == [x₀]Q + g2.AssertIsEqual(xQ, psiQ) +} + +// Select selects between p and q given the selector b. If b == 1, then returns +// p and q otherwise. +func (g2 *G2) Select(b frontend.Variable, p, q *G2Affine) *G2Affine { + x := g2.Ext2.Select(b, &p.P.X, &q.P.X) + y := g2.Ext2.Select(b, &p.P.Y, &q.P.Y) + return &G2Affine{ + P: g2AffP{X: *x, Y: *y}, + Lines: nil, + } +} + // AssertIsEqual asserts that p and q are the same point. func (g2 *G2) AssertIsEqual(p, q *G2Affine) { g2.Ext2.AssertIsEqual(&p.P.X, &q.P.X) g2.Ext2.AssertIsEqual(&p.P.Y, &q.P.Y) } + +func (g2 *G2) IsEqual(p, q *G2Affine) frontend.Variable { + xEqual := g2.Ext2.IsEqual(&p.P.X, &q.P.X) + yEqual := g2.Ext2.IsEqual(&p.P.Y, &q.P.Y) + return g2.api.And(xEqual, yEqual) +} + +// scalarMulGeneric computes [s]p and returns it. It doesn't modify p nor s. +// This function doesn't check that the p is on the curve. See AssertIsOnCurve. +// +// ⚠️ p must not be (0,0) and s must not be 0, unless [algopts.WithCompleteArithmetic] option is set. +// (0,0) is not on the curve but we conventionally take it as the +// neutral/infinity point as per the [EVM]. +// +// It computes the right-to-left variable-base double-and-add algorithm ([Joye07], Alg.1). +// +// Since we use incomplete formulas for the addition law, we need to start with +// a non-zero accumulator point (R0). To do this, we skip the LSB (bit at +// position 0) and proceed assuming it was 1. At the end, we conditionally +// subtract the initial value (p) if LSB is 1. We also handle the bits at +// positions 1 and n-1 outside of the loop to optimize the number of +// constraints using [ELM03] (Section 3.1) +// +// [ELM03]: https://arxiv.org/pdf/math/0208038.pdf +// [EVM]: https://ethereum.github.io/yellowpaper/paper.pdf +// [Joye07]: https://www.iacr.org/archive/ches2007/47270135/47270135.pdf +func (g2 *G2) scalarMulGeneric(p *G2Affine, s *Scalar, opts ...algopts.AlgebraOption) *G2Affine { + cfg, err := algopts.NewConfig(opts...) + if err != nil { + panic(fmt.Sprintf("parse opts: %v", err)) + } + var selector frontend.Variable + if cfg.CompleteArithmetic { + // if p=(0,0) we assign a dummy (0,1) to p and continue + selector = g2.api.And(g2.Ext2.IsZero(&p.P.X), g2.Ext2.IsZero(&p.P.Y)) + one := g2.Ext2.One() + p = g2.Select(selector, &G2Affine{P: g2AffP{X: *one, Y: *one}, Lines: nil}, p) + } + + var st ScalarField + sBits := g2.fr.ToBitsCanonical(s) + n := st.Modulus().BitLen() + if cfg.NbScalarBits > 2 && cfg.NbScalarBits < n { + n = cfg.NbScalarBits + } + + // i = 1 + Rb := g2.triple(p) + R0 := g2.Select(sBits[1], Rb, p) + R1 := g2.Select(sBits[1], p, Rb) + + for i := 2; i < n-1; i++ { + Rb = g2.doubleAndAddSelect(sBits[i], R0, R1) + R0 = g2.Select(sBits[i], Rb, R0) + R1 = g2.Select(sBits[i], R1, Rb) + } + + // i = n-1 + Rb = g2.doubleAndAddSelect(sBits[n-1], R0, R1) + R0 = g2.Select(sBits[n-1], Rb, R0) + + // i = 0 + // we use AddUnified instead of Add. This is because: + // - when s=0 then R0=P and AddUnified(P, -P) = (0,0). We return (0,0). + // - when s=1 then R0=P AddUnified(Q, -Q) is well defined. We return R0=P. + R0 = g2.Select(sBits[0], R0, g2.AddUnified(R0, g2.neg(p))) + + if cfg.CompleteArithmetic { + // if p=(0,0), return (0,0) + zero := g2.Ext2.Zero() + R0 = g2.Select(selector, &G2Affine{P: g2AffP{X: *zero, Y: *zero}, Lines: nil}, R0) + } + + return R0 +} + +// scalarMulGLV computes [s]Q using an efficient endomorphism and returns it. It doesn't modify Q nor s. +// It implements an optimized version based on algorithm 1 of [Halo] (see Section 6.2 and appendix C). +// +// ⚠️ The scalar s must be nonzero and the point Q different from (0,0) unless [algopts.WithCompleteArithmetic] is set. +// (0,0) is not on the curve but we conventionally take it as the +// neutral/infinity point as per the [EVM]. +// +// [Halo]: https://eprint.iacr.org/2019/1021.pdf +// [EVM]: https://ethereum.github.io/yellowpaper/paper.pdf +func (g2 *G2) scalarMulGLV(Q *G2Affine, s *Scalar, opts ...algopts.AlgebraOption) *G2Affine { + cfg, err := algopts.NewConfig(opts...) + if err != nil { + panic(err) + } + addFn := g2.add + var selector frontend.Variable + if cfg.CompleteArithmetic { + addFn = g2.AddUnified + // if Q=(0,0) we assign a dummy (1,1) to Q and continue + selector = g2.api.And( + g2.api.And(g2.fp.IsZero(&Q.P.X.A0), g2.fp.IsZero(&Q.P.X.A1)), + g2.api.And(g2.fp.IsZero(&Q.P.Y.A0), g2.fp.IsZero(&Q.P.Y.A1)), + ) + one := g2.Ext2.One() + Q = g2.Select(selector, &G2Affine{P: g2AffP{X: *one, Y: *one}, Lines: nil}, Q) + } + + // We use the endomorphism à la GLV to compute [s]Q as + // [s1]Q + [s2]Φ(Q) + // the sub-scalars s1, s2 can be negative (bigints) in the hint. If so, + // they will be reduced in-circuit modulo the SNARK scalar field and not + // the emulated field. So we return in the hint |s1|, |s2| and boolean + // flags sdBits to negate the points Q, Φ(Q) instead of the corresponding + // sub-scalars. + + // decompose s into s1 and s2 + sd, err := g2.fr.NewHint(decomposeScalarG1Subscalars, 2, s, g2.eigenvalue) + if err != nil { + panic(fmt.Sprintf("compute GLV decomposition: %v", err)) + } + s1, s2 := sd[0], sd[1] + sdBits, err := g2.fr.NewHintWithNativeOutput(decomposeScalarG1Signs, 2, s, g2.eigenvalue) + if err != nil { + panic(fmt.Sprintf("compute GLV decomposition bits: %v", err)) + } + selector1, selector2 := sdBits[0], sdBits[1] + s3 := g2.fr.Select(selector1, g2.fr.Neg(s1), s1) + s4 := g2.fr.Select(selector2, g2.fr.Neg(s2), s2) + // s == s3 + [λ]s4 + g2.fr.AssertIsEqual( + g2.fr.Add(s3, g2.fr.Mul(s4, g2.eigenvalue)), + s, + ) + + s1bits := g2.fr.ToBits(s1) + s2bits := g2.fr.ToBits(s2) + + // precompute -Q, -Φ(Q), Φ(Q) + var tableQ, tablePhiQ [3]*G2Affine + negQY := g2.Ext2.Neg(&Q.P.Y) + tableQ[1] = &G2Affine{ + P: g2AffP{ + X: Q.P.X, + Y: *g2.Ext2.Select(selector1, negQY, &Q.P.Y), + }, + } + tableQ[0] = g2.neg(tableQ[1]) + tablePhiQ[1] = &G2Affine{ + P: g2AffP{ + X: *g2.Ext2.MulByElement(&Q.P.X, g2.w2), + Y: *g2.Ext2.Select(selector2, negQY, &Q.P.Y), + }, + } + tablePhiQ[0] = g2.neg(tablePhiQ[1]) + tableQ[2] = g2.triple(tableQ[1]) + tablePhiQ[2] = &G2Affine{ + P: g2AffP{ + X: *g2.Ext2.MulByElement(&tableQ[2].P.X, g2.w2), + Y: *g2.Ext2.Select(selector2, g2.Ext2.Neg(&tableQ[2].P.Y), &tableQ[2].P.Y), + }, + } + + // we suppose that the first bits of the sub-scalars are 1 and set: + // Acc = Q + Φ(Q) + Acc := g2.add(tableQ[1], tablePhiQ[1]) + + // At each iteration we need to compute: + // [2]Acc ± Q ± Φ(Q). + // We can compute [2]Acc and look up the (precomputed) point P from: + // B1 = Q+Φ(Q) + // B2 = -Q-Φ(Q) + // B3 = Q-Φ(Q) + // B4 = -Q+Φ(Q) + // + // If we extend this by merging two iterations, we need to look up P and P' + // both from {B1, B2, B3, B4} and compute: + // [2]([2]Acc+P)+P' = [4]Acc + T + // where T = [2]P+P'. So at each (merged) iteration, we can compute [4]Acc + // and look up T from the precomputed list of points: + // + // T = [3](Q + Φ(Q)) + // P = B1 and P' = B1 + T1 := g2.add(tableQ[2], tablePhiQ[2]) + // T = Q + Φ(Q) + // P = B1 and P' = B2 + T2 := Acc + // T = [3]Q + Φ(Q) + // P = B1 and P' = B3 + T3 := g2.add(tableQ[2], tablePhiQ[1]) + // T = Q + [3]Φ(Q) + // P = B1 and P' = B4 + T4 := g2.add(tableQ[1], tablePhiQ[2]) + // T = -Q - Φ(Q) + // P = B2 and P' = B1 + T5 := g2.neg(T2) + // T = -[3](Q + Φ(Q)) + // P = B2 and P' = B2 + T6 := g2.neg(T1) + // T = -Q - [3]Φ(Q) + // P = B2 and P' = B3 + T7 := g2.neg(T4) + // T = -[3]Q - Φ(Q) + // P = B2 and P' = B4 + T8 := g2.neg(T3) + // T = [3]Q - Φ(Q) + // P = B3 and P' = B1 + T9 := g2.add(tableQ[2], tablePhiQ[0]) + // T = Q - [3]Φ(Q) + // P = B3 and P' = B2 + T11 := g2.neg(tablePhiQ[2]) + T10 := g2.add(tableQ[1], T11) + // T = [3](Q - Φ(Q)) + // P = B3 and P' = B3 + T11 = g2.add(tableQ[2], T11) + // T = -Φ(Q) + Q + // P = B3 and P' = B4 + T12 := g2.add(tablePhiQ[0], tableQ[1]) + // T = [3]Φ(Q) - Q + // P = B4 and P' = B1 + T13 := g2.neg(T10) + // T = Φ(Q) - [3]Q + // P = B4 and P' = B2 + T14 := g2.neg(T9) + // T = Φ(Q) - Q + // P = B4 and P' = B3 + T15 := g2.neg(T12) + // T = [3](Φ(Q) - Q) + // P = B4 and P' = B4 + T16 := g2.neg(T11) + // note that half the points are negatives of the other half, + // hence have the same X coordinates. + + nbits := 130 + for i := nbits - 2; i > 0; i -= 2 { + // selectorY takes values in [0,15] + selectorY := g2.api.Add( + s1bits[i], + g2.api.Mul(s2bits[i], 2), + g2.api.Mul(s1bits[i-1], 4), + g2.api.Mul(s2bits[i-1], 8), + ) + // selectorX takes values in [0,7] s.t.: + // - when selectorY < 8: selectorX = selectorY + // - when selectorY >= 8: selectorX = 15 - selectorY + selectorX := g2.api.Add( + g2.api.Mul(selectorY, g2.api.Sub(1, g2.api.Mul(s2bits[i-1], 2))), + g2.api.Mul(s2bits[i-1], 15), + ) + // Bi.Y are distincts so we need a 16-to-1 multiplexer, + // but only half of the Bi.X are distinct so we need a 8-to-1. + T := &G2Affine{ + P: g2AffP{ + X: fields_bls12381.E2{ + A0: *g2.fp.Mux(selectorX, &T6.P.X.A0, &T10.P.X.A0, &T14.P.X.A0, &T2.P.X.A0, &T7.P.X.A0, &T11.P.X.A0, &T15.P.X.A0, &T3.P.X.A0), + A1: *g2.fp.Mux(selectorX, &T6.P.X.A1, &T10.P.X.A1, &T14.P.X.A1, &T2.P.X.A1, &T7.P.X.A1, &T11.P.X.A1, &T15.P.X.A1, &T3.P.X.A1), + }, + Y: fields_bls12381.E2{ + A0: *g2.fp.Mux(selectorY, + &T6.P.Y.A0, &T10.P.Y.A0, &T14.P.Y.A0, &T2.P.Y.A0, &T7.P.Y.A0, &T11.P.Y.A0, &T15.P.Y.A0, &T3.P.Y.A0, + &T8.P.Y.A0, &T12.P.Y.A0, &T16.P.Y.A0, &T4.P.Y.A0, &T5.P.Y.A0, &T9.P.Y.A0, &T13.P.Y.A0, &T1.P.Y.A0, + ), + A1: *g2.fp.Mux(selectorY, + &T6.P.Y.A1, &T10.P.Y.A1, &T14.P.Y.A1, &T2.P.Y.A1, &T7.P.Y.A1, &T11.P.Y.A1, &T15.P.Y.A1, &T3.P.Y.A1, + &T8.P.Y.A1, &T12.P.Y.A1, &T16.P.Y.A1, &T4.P.Y.A1, &T5.P.Y.A1, &T9.P.Y.A1, &T13.P.Y.A1, &T1.P.Y.A1, + ), + }, + }, + } + // Acc = [4]Acc + T + Acc = g2.double(Acc) + Acc = g2.doubleAndAdd(Acc, T) + } + + // i = 0 + // subtract the Q, Φ(Q) if the first bits are 0. + // When cfg.CompleteArithmetic is set, we use AddUnified instead of Add. + // This means when s=0 then Acc=(0,0) because AddUnified(Q, -Q) = (0,0). + tableQ[0] = addFn(tableQ[0], Acc) + Acc = g2.Select(s1bits[0], Acc, tableQ[0]) + tablePhiQ[0] = addFn(tablePhiQ[0], Acc) + Acc = g2.Select(s2bits[0], Acc, tablePhiQ[0]) + + if cfg.CompleteArithmetic { + zero := g2.Ext2.Zero() + Acc = g2.Select(selector, &G2Affine{P: g2AffP{X: *zero, Y: *zero}}, Acc) + } + + return Acc +} + +// MultiScalarMul computes the multi scalar multiplication of the points P and +// scalars s. It returns an error if the length of the slices mismatch. If the +// input slices are empty, then returns point at infinity. +func (g2 *G2) MultiScalarMul(p []*G2Affine, s []*Scalar, opts ...algopts.AlgebraOption) (*G2Affine, error) { + + if len(p) == 0 { + return &G2Affine{ + P: g2AffP{ + X: *g2.Ext2.Zero(), + Y: *g2.Ext2.Zero(), + }, + Lines: nil, + }, nil + } + cfg, err := algopts.NewConfig(opts...) + if err != nil { + return nil, fmt.Errorf("new config: %w", err) + } + addFn := g2.add + if cfg.CompleteArithmetic { + addFn = g2.AddUnified + } + if !cfg.FoldMulti { + // the scalars are unique + if len(p) != len(s) { + return nil, fmt.Errorf("mismatching points and scalars slice lengths") + } + n := len(p) + res := g2.scalarMulGLV(p[0], s[0], opts...) + for i := 1; i < n; i++ { + q := g2.scalarMulGLV(p[i], s[i], opts...) + res = addFn(res, q) + } + return res, nil + } else { + // scalars are powers + if len(s) == 0 { + return nil, fmt.Errorf("need scalar for folding") + } + gamma := s[0] + res := g2.scalarMulGLV(p[len(p)-1], gamma, opts...) + for i := len(p) - 2; i > 0; i-- { + res = addFn(p[i], res) + res = g2.scalarMulGLV(res, gamma, opts...) + } + res = addFn(p[0], res) + return res, nil + } +} diff --git a/std/algebra/emulated/sw_bls12381/g2_test.go b/std/algebra/emulated/sw_bls12381/g2_test.go index 9d4a90d0..73564333 100644 --- a/std/algebra/emulated/sw_bls12381/g2_test.go +++ b/std/algebra/emulated/sw_bls12381/g2_test.go @@ -1,22 +1,65 @@ package sw_bls12381 import ( + "fmt" "math/big" "testing" "github.com/consensys/gnark-crypto/ecc" bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" + fr_bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/emulated/fields_bls12381" + "github.com/consensys/gnark/std/math/emulated" "github.com/consensys/gnark/test" ) +type mulG2Circuit struct { + In, Res G2Affine + S Scalar +} + +func (c *mulG2Circuit) Define(api frontend.API) error { + g2, err := NewG2(api) + if err != nil { + return fmt.Errorf("new G2 struct: %w", err) + } + res1 := g2.scalarMulGLV(&c.In, &c.S) + res2 := g2.scalarMulGeneric(&c.In, &c.S) + g2.AssertIsEqual(res1, &c.Res) + g2.AssertIsEqual(res2, &c.Res) + return nil +} + +func TestScalarMulG2TestSolve(t *testing.T) { + assert := test.NewAssert(t) + var r fr_bls12381.Element + _, _ = r.SetRandom() + s := new(big.Int) + r.BigInt(s) + var res bls12381.G2Affine + _, _, _, gen := bls12381.Generators() + res.ScalarMultiplication(&gen, s) + + witness := mulG2Circuit{ + In: NewG2Affine(gen), + S: NewScalar(r), + Res: NewG2Affine(res), + } + err := test.IsSolved(&mulG2Circuit{}, &witness, ecc.BN254.ScalarField()) + assert.NoError(err) +} + type addG2Circuit struct { In1, In2 G2Affine Res G2Affine } func (c *addG2Circuit) Define(api frontend.API) error { - g2 := NewG2(api) + g2, err := NewG2(api) + if err != nil { + return fmt.Errorf("new G2 struct: %w", err) + } res := g2.add(&c.In1, &c.In2) g2.AssertIsEqual(res, &c.Res) return nil @@ -43,7 +86,10 @@ type doubleG2Circuit struct { } func (c *doubleG2Circuit) Define(api frontend.API) error { - g2 := NewG2(api) + g2, err := NewG2(api) + if err != nil { + return fmt.Errorf("new G2 struct: %w", err) + } res := g2.double(&c.In1) g2.AssertIsEqual(res, &c.Res) return nil @@ -71,7 +117,10 @@ type doubleAndAddG2Circuit struct { } func (c *doubleAndAddG2Circuit) Define(api frontend.API) error { - g2 := NewG2(api) + g2, err := NewG2(api) + if err != nil { + return fmt.Errorf("new G2 struct: %w", err) + } res := g2.doubleAndAdd(&c.In1, &c.In2) g2.AssertIsEqual(res, &c.Res) return nil @@ -99,7 +148,10 @@ type scalarMulG2BySeedCircuit struct { } func (c *scalarMulG2BySeedCircuit) Define(api frontend.API) error { - g2 := NewG2(api) + g2, err := NewG2(api) + if err != nil { + return fmt.Errorf("new G2 struct: %w", err) + } res := g2.scalarMulBySeed(&c.In1) g2.AssertIsEqual(res, &c.Res) return nil @@ -118,3 +170,75 @@ func TestScalarMulG2BySeedTestSolve(t *testing.T) { err := test.IsSolved(&scalarMulG2BySeedCircuit{}, &witness, ecc.BN254.ScalarField()) assert.NoError(err) } + +type MultiScalarMulTest struct { + Points []G2Affine + Scalars []Scalar + Res G2Affine +} + +func (c *MultiScalarMulTest) Define(api frontend.API) error { + g2, err := NewG2(api) + if err != nil { + return fmt.Errorf("new G2 struct: %w", err) + } + ps := make([]*G2Affine, len(c.Points)) + for i := range c.Points { + ps[i] = &c.Points[i] + } + ss := make([]*Scalar, len(c.Scalars)) + for i := range c.Scalars { + ss[i] = &c.Scalars[i] + } + res, err := g2.MultiScalarMul(ps, ss) + if err != nil { + return err + } + g2.AssertIsEqual(res, &c.Res) + return nil +} + +func TestMultiScalarMul(t *testing.T) { + assert := test.NewAssert(t) + nbLen := 4 + P := make([]bls12381.G2Affine, nbLen) + S := make([]fr_bls12381.Element, nbLen) + for i := 0; i < nbLen; i++ { + S[i].SetRandom() + P[i].ScalarMultiplicationBase(S[i].BigInt(new(big.Int))) + } + var res bls12381.G2Affine + _, err := res.MultiExp(P, S, ecc.MultiExpConfig{}) + + assert.NoError(err) + cP := make([]G2Affine, len(P)) + for i := range cP { + cP[i] = G2Affine{ + P: g2AffP{ + X: fields_bls12381.E2{A0: emulated.ValueOf[emulated.BLS12381Fp](P[i].X.A0), A1: emulated.ValueOf[emulated.BLS12381Fp](P[i].X.A1)}, + Y: fields_bls12381.E2{A0: emulated.ValueOf[emulated.BLS12381Fp](P[i].Y.A0), A1: emulated.ValueOf[emulated.BLS12381Fp](P[i].Y.A1)}, + }, + Lines: nil, + } + } + cS := make([]Scalar, len(S)) + for i := range cS { + cS[i] = emulated.ValueOf[emulated.BLS12381Fr](S[i]) + } + assignment := MultiScalarMulTest{ + Points: cP, + Scalars: cS, + Res: G2Affine{ + P: g2AffP{ + X: fields_bls12381.E2{A0: emulated.ValueOf[emulated.BLS12381Fp](res.X.A0), A1: emulated.ValueOf[emulated.BLS12381Fp](res.X.A1)}, + Y: fields_bls12381.E2{A0: emulated.ValueOf[emulated.BLS12381Fp](res.Y.A0), A1: emulated.ValueOf[emulated.BLS12381Fp](res.Y.A1)}, + }, + Lines: nil, + }, + } + err = test.IsSolved(&MultiScalarMulTest{ + Points: make([]G2Affine, nbLen), + Scalars: make([]Scalar, nbLen), + }, &assignment, ecc.BN254.ScalarField()) + assert.NoError(err) +} diff --git a/std/algebra/emulated/sw_bls12381/hints.go b/std/algebra/emulated/sw_bls12381/hints.go index b767113d..928ea93c 100644 --- a/std/algebra/emulated/sw_bls12381/hints.go +++ b/std/algebra/emulated/sw_bls12381/hints.go @@ -1,9 +1,14 @@ package sw_bls12381 import ( + "errors" + "fmt" "math/big" + "github.com/consensys/gnark-crypto/ecc" bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fp" + "github.com/consensys/gnark-crypto/ecc/bls12-381/hash_to_curve" "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/std/math/emulated" ) @@ -17,6 +22,11 @@ func GetHints() []solver.Hint { return []solver.Hint{ finalExpHint, pairingCheckHint, + millerLoopAndCheckFinalExpHint, + decomposeScalarG1Subscalars, + decomposeScalarG1Signs, + g1SqrtRatioHint, + g2SqrtRatioHint, } } @@ -200,3 +210,178 @@ func finalExpWitness(millerLoop *bls12381.E12) (residueWitness, scalingFactor bl return residueWitness, scalingFactor } + +func millerLoopAndCheckFinalExpHint(nativeMod *big.Int, nativeInputs, nativeOutputs []*big.Int) error { + return emulated.UnwrapHint(nativeInputs, nativeOutputs, + func(mod *big.Int, inputs, outputs []*big.Int) error { + var P bls12381.G1Affine + var Q bls12381.G2Affine + var previous bls12381.E12 + + P.X.SetBigInt(inputs[0]) + P.Y.SetBigInt(inputs[1]) + Q.X.A0.SetBigInt(inputs[2]) + Q.X.A1.SetBigInt(inputs[3]) + Q.Y.A0.SetBigInt(inputs[4]) + Q.Y.A1.SetBigInt(inputs[5]) + + previous.C0.B0.A0.SetBigInt(inputs[6]) + previous.C0.B0.A1.SetBigInt(inputs[7]) + previous.C0.B1.A0.SetBigInt(inputs[8]) + previous.C0.B1.A1.SetBigInt(inputs[9]) + previous.C0.B2.A0.SetBigInt(inputs[10]) + previous.C0.B2.A1.SetBigInt(inputs[11]) + previous.C1.B0.A0.SetBigInt(inputs[12]) + previous.C1.B0.A1.SetBigInt(inputs[13]) + previous.C1.B1.A0.SetBigInt(inputs[14]) + previous.C1.B1.A1.SetBigInt(inputs[15]) + previous.C1.B2.A0.SetBigInt(inputs[16]) + previous.C1.B2.A1.SetBigInt(inputs[17]) + + if previous.IsZero() { + return errors.New("previous Miller loop result is zero") + } + + lines := bls12381.PrecomputeLines(Q) + millerLoop, err := bls12381.MillerLoopFixedQ( + []bls12381.G1Affine{P}, + [][2][len(bls12381.LoopCounter) - 1]bls12381.LineEvaluationAff{lines}, + ) + if err != nil { + return err + } + millerLoop.Conjugate(&millerLoop) + + millerLoop.Mul(&millerLoop, &previous) + + residueWitnessInv, scalingFactor := finalExpWitness(&millerLoop) + residueWitnessInv.Inverse(&residueWitnessInv) + + residueWitnessInv.C0.B0.A0.BigInt(outputs[0]) + residueWitnessInv.C0.B0.A1.BigInt(outputs[1]) + residueWitnessInv.C0.B1.A0.BigInt(outputs[2]) + residueWitnessInv.C0.B1.A1.BigInt(outputs[3]) + residueWitnessInv.C0.B2.A0.BigInt(outputs[4]) + residueWitnessInv.C0.B2.A1.BigInt(outputs[5]) + residueWitnessInv.C1.B0.A0.BigInt(outputs[6]) + residueWitnessInv.C1.B0.A1.BigInt(outputs[7]) + residueWitnessInv.C1.B1.A0.BigInt(outputs[8]) + residueWitnessInv.C1.B1.A1.BigInt(outputs[9]) + residueWitnessInv.C1.B2.A0.BigInt(outputs[10]) + residueWitnessInv.C1.B2.A1.BigInt(outputs[11]) + + // return the scaling factor + scalingFactor.C0.B0.A0.BigInt(outputs[12]) + scalingFactor.C0.B0.A1.BigInt(outputs[13]) + scalingFactor.C0.B1.A0.BigInt(outputs[14]) + scalingFactor.C0.B1.A1.BigInt(outputs[15]) + scalingFactor.C0.B2.A0.BigInt(outputs[16]) + scalingFactor.C0.B2.A1.BigInt(outputs[17]) + + return nil + }) +} + +func decomposeScalarG1Subscalars(mod *big.Int, inputs []*big.Int, outputs []*big.Int) error { + return emulated.UnwrapHint(inputs, outputs, func(field *big.Int, inputs, outputs []*big.Int) error { + if len(inputs) != 2 { + return fmt.Errorf("expecting two inputs") + } + if len(outputs) != 2 { + return fmt.Errorf("expecting two outputs") + } + glvBasis := new(ecc.Lattice) + ecc.PrecomputeLattice(field, inputs[1], glvBasis) + sp := ecc.SplitScalar(inputs[0], glvBasis) + outputs[0].Set(&(sp[0])) + outputs[1].Set(&(sp[1])) + // we need the absolute values for the in-circuit computations, + // otherwise the negative values will be reduced modulo the SNARK scalar + // field and not the emulated field. + // output0 = |s0| mod r + // output1 = |s1| mod r + if outputs[0].Sign() == -1 { + outputs[0].Neg(outputs[0]) + } + if outputs[1].Sign() == -1 { + outputs[1].Neg(outputs[1]) + } + + return nil + }) +} + +func decomposeScalarG1Signs(mod *big.Int, inputs []*big.Int, outputs []*big.Int) error { + return emulated.UnwrapHintWithNativeOutput(inputs, outputs, func(field *big.Int, inputs, outputs []*big.Int) error { + if len(inputs) != 2 { + return fmt.Errorf("expecting two inputs") + } + if len(outputs) != 2 { + return fmt.Errorf("expecting two outputs") + } + glvBasis := new(ecc.Lattice) + ecc.PrecomputeLattice(field, inputs[1], glvBasis) + sp := ecc.SplitScalar(inputs[0], glvBasis) + outputs[0].SetUint64(0) + if sp[0].Sign() == -1 { + outputs[0].SetUint64(1) + } + outputs[1].SetUint64(0) + if sp[1].Sign() == -1 { + outputs[1].SetUint64(1) + } + + return nil + }) +} + +// g1SqrtRatio computes the square root of u/v and returns 0 iff u/v was indeed a quadratic residue +// if not, we get sqrt(Z * u / v). Recall that Z is non-residue +// If v = 0, u/v is meaningless and the output is unspecified, without raising an error. +// The main idea is that since the computation of the square root involves taking large powers of u/v, the inversion of v can be avoided. +// +// nativeInputs[0] = u, nativeInputs[1]=v +// nativeOutput[1] = 1 if u/v is a QR, 0 otherwise, nativeOutput[1]=sqrt(u/v) or sqrt(Z u/v) +func g1SqrtRatioHint(nativeMod *big.Int, nativeInputs, nativeOutputs []*big.Int) error { + return emulated.UnwrapHint(nativeInputs, nativeOutputs, + func(mod *big.Int, inputs, outputs []*big.Int) error { + var u, v, z fp.Element + u.SetBigInt(inputs[0]) + v.SetBigInt(inputs[1]) + + isQNr := hash_to_curve.G1SqrtRatio(&z, &u, &v) + if isQNr != 0 { + isQNr = 1 + } + z.BigInt(outputs[0]) + outputs[1].SetInt64(int64(isQNr)) + return nil + }) +} + +func g2SqrtRatioHint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { + return emulated.UnwrapHint(inputs, outputs, func(field *big.Int, inputs, outputs []*big.Int) error { + if len(inputs) != 4 { + return fmt.Errorf("expecting 4 inputs") + } + if len(outputs) != 3 { + return fmt.Errorf("expecting 3 outputs") + } + + var z, u, v bls12381.E2 + u.A0.SetBigInt(inputs[0]) + u.A1.SetBigInt(inputs[1]) + v.A0.SetBigInt(inputs[2]) + v.A1.SetBigInt(inputs[3]) + + isQNr := hash_to_curve.G2SqrtRatio(&z, &u, &v) + if isQNr != 0 { + isQNr = 1 + } + + outputs[0].SetUint64(isQNr) + z.A0.BigInt(outputs[1]) + z.A1.BigInt(outputs[2]) + return nil + }) +} diff --git a/std/algebra/emulated/sw_bls12381/map_to_g1.go b/std/algebra/emulated/sw_bls12381/map_to_g1.go new file mode 100644 index 00000000..20c908c4 --- /dev/null +++ b/std/algebra/emulated/sw_bls12381/map_to_g1.go @@ -0,0 +1,182 @@ +package sw_bls12381 + +import ( + "fmt" + + "github.com/consensys/gnark-crypto/ecc/bls12-381/fp" + "github.com/consensys/gnark-crypto/ecc/bls12-381/hash_to_curve" + "github.com/consensys/gnark/frontend" +) + +func (g1 *G1) evalFixedPolynomial(monic bool, coefficients []fp.Element, x *baseEl) *baseEl { + emuCoefficients := make([]*baseEl, len(coefficients)) + for i := range coefficients { + emuCoefficients[i] = g1.curveF.NewElement(coefficients[i]) + } + var res *baseEl + if monic { + res = g1.curveF.Add(emuCoefficients[len(emuCoefficients)-1], x) + } else { + res = emuCoefficients[len(emuCoefficients)-1] + } + + for i := len(emuCoefficients) - 2; i >= 0; i-- { + res = g1.curveF.Mul(res, x) + res = g1.curveF.Add(res, emuCoefficients[i]) + } + return res + +} + +func (g1 *G1) isogeny(p *G1Affine) *G1Affine { + isogenyMap := hash_to_curve.G1IsogenyMap() + ydenom := g1.evalFixedPolynomial(true, isogenyMap[3], &p.X) + xdenom := g1.evalFixedPolynomial(true, isogenyMap[1], &p.X) + y := g1.evalFixedPolynomial(false, isogenyMap[2], &p.X) + y = g1.curveF.Mul(y, &p.Y) + x := g1.evalFixedPolynomial(false, isogenyMap[0], &p.X) + x = g1.curveF.Div(x, xdenom) + y = g1.curveF.Div(y, ydenom) + return &G1Affine{X: *x, Y: *y} +} + +// g1Sgn0 returns the parity of a +func (g1 *G1) sgn0(a *baseEl) frontend.Variable { + ab := g1.curveF.ToBitsCanonical(a) + return ab[0] +} + +// ClearCofactor clears the cofactor of a point in G1. +// +// See: https://eprint.iacr.org/2019/403.pdf, 5 +func (g1 *G1) ClearCofactor(q *G1Affine) *G1Affine { + // cf https://eprint.iacr.org/2019/403.pdf, 5 + + // mulBySeed + z := g1.double(q) + z = g1.add(z, q) + z = g1.double(z) + z = g1.doubleAndAdd(z, q) + z = g1.doubleN(z, 2) + z = g1.doubleAndAdd(z, q) + z = g1.doubleN(z, 8) + z = g1.doubleAndAdd(z, q) + z = g1.doubleN(z, 31) + z = g1.doubleAndAdd(z, q) + z = g1.doubleN(z, 16) + + // Add assign + z = g1.add(z, q) + + return z +} + +// MapToCurve1 implements the SSWU map. It does not perform cofactor clearing or isogeny computation. +// See [G1.MapToG1] for the complete map to G1. +// +// See: https://www.ietf.org/archive/id/draft-irtf-cfrg-hash-to-curve-16.html#name-simplified-swu-method +func (g1 *G1) MapToCurve1(u *baseEl) (*G1Affine, error) { + one := g1.curveF.One() + z := g1.curveF.NewElement(hash_to_curve.G1SSWUIsogenyZ()) + + sswuIsoCurveCoeffAValue, sswuIsoCurveCoeffBValue := hash_to_curve.G1SSWUIsogenyCurveCoefficients() + sswuIsoCurveCoeffA := g1.curveF.NewElement(sswuIsoCurveCoeffAValue) + sswuIsoCurveCoeffB := g1.curveF.NewElement(sswuIsoCurveCoeffBValue) + + tv1 := g1.curveF.Mul(u, u) // 1. tv1 = u² + + //mul tv1 by Z ( g1MulByZ) + tv1 = g1.curveF.Mul(z, tv1) + + // var tv2 fp.Element + tv2 := g1.curveF.Mul(tv1, tv1) // 3. tv2 = tv1² + tv2 = g1.curveF.Add(tv2, tv1) // 4. tv2 = tv2 + tv1 + + // var tv3 fp.Element + // var tv4 fp.Element + tv3 := g1.curveF.Add(tv2, one) // 5. tv3 = tv2 + 1 + tv3 = g1.curveF.Mul(tv3, sswuIsoCurveCoeffB) // 6. tv3 = B * tv3 + + // tv2NZero := g1NotZero(&tv2) + tv2IsZero := g1.curveF.IsZero(tv2) + + // tv4 = Z + + tv2 = g1.curveF.Neg(tv2) // tv2.Neg(&tv2) + tv4 := g1.curveF.Select(tv2IsZero, z, tv2) // 7. tv4 = CMOV(Z, -tv2, tv2 != 0) + tv4 = g1.curveF.Mul(tv4, sswuIsoCurveCoeffA) // 8. tv4 = A * tv4 + + tv2 = g1.curveF.Mul(tv3, tv3) // 9. tv2 = tv3² + + tv6 := g1.curveF.Mul(tv4, tv4) // 10. tv6 = tv4² + + tv5 := g1.curveF.Mul(tv6, sswuIsoCurveCoeffA) // 11. tv5 = A * tv6 + + tv2 = g1.curveF.Add(tv2, tv5) // 12. tv2 = tv2 + tv5 + tv2 = g1.curveF.Mul(tv2, tv3) // 13. tv2 = tv2 * tv3 + tv6 = g1.curveF.Mul(tv6, tv4) // 14. tv6 = tv6 * tv4 + + tv5 = g1.curveF.Mul(tv6, sswuIsoCurveCoeffB) // 15. tv5 = B * tv6 + tv2 = g1.curveF.Add(tv2, tv5) // 16. tv2 = tv2 + tv5 + + x := g1.curveF.Mul(tv1, tv3) // 17. x = tv1 * tv3 + + hint, err := g1.curveF.NewHint(g1SqrtRatioHint, 2, tv2, tv6) + if err != nil { + return nil, err + } + + y1 := hint[0] // 18. (is_gx1_square, y1) = sqrt_ratio(tv2, tv6) + + // (gx1NSquare==1 AND (u/v) QNR ) OR (gx1NSquare==0 AND (u/v) QR ) + gx1NSquare := hint[1].Limbs[0] + + g1.api.AssertIsBoolean(gx1NSquare) + y1Squarev := g1.curveF.Mul(y1, y1) + y1Squarev = g1.curveF.Mul(y1Squarev, tv6) + uz := g1.curveF.Mul(tv2, z) + ysvMinusuz := g1.curveF.Sub(y1Squarev, uz) + isQNRWitness := g1.curveF.IsZero(ysvMinusuz) + cond1 := g1.api.And(isQNRWitness, gx1NSquare) + + ysvMinusu := g1.curveF.Sub(y1Squarev, tv2) + isQRWitness := g1.curveF.IsZero(ysvMinusu) + isQR := g1.api.Sub(1, gx1NSquare) + cond2 := g1.api.And(isQR, isQRWitness) + + cond := g1.api.Xor(cond1, cond2) + g1.api.AssertIsEqual(cond, 1) + + // var y fp.Element + y := g1.curveF.Mul(tv1, u) // 19. y = tv1 * u + + y = g1.curveF.Mul(y, y1) // 20. y = y * y1 + + x = g1.curveF.Select(gx1NSquare, x, tv3) // 21. x = CMOV(x, tv3, is_gx1_square) + y = g1.curveF.Select(gx1NSquare, y, y1) // 22. y = CMOV(y, y1, is_gx1_square) + + y1 = g1.curveF.Neg(y) + y1 = g1.curveF.Reduce(y1) + sel := g1.api.IsZero(g1.api.Sub(g1.sgn0(u), g1.sgn0(y))) + y = g1.curveF.Select(sel, y, y1) + + // // 23. e1 = sgn0(u) == sgn0(y) + // // 24. y = CMOV(-y, y, e1) + + x = g1.curveF.Div(x, tv4) // 25. x = x / tv4 + + return &G1Affine{X: *x, Y: *y}, nil + +} + +// MapToG1 invokes the SSWU map, and guarantees that the result is in G1. For +// variant without cofactor clearing and isogeny, see [G1.MapToCurve1]. +func (g1 *G1) MapToG1(u *baseEl) (*G1Affine, error) { + res, err := g1.MapToCurve1(u) + if err != nil { + return nil, fmt.Errorf("map to curve: %w", err) + } + z := g1.isogeny(res) + z = g1.ClearCofactor(z) + return z, nil +} diff --git a/std/algebra/emulated/sw_bls12381/map_to_g1_test.go b/std/algebra/emulated/sw_bls12381/map_to_g1_test.go new file mode 100644 index 00000000..24d63e5f --- /dev/null +++ b/std/algebra/emulated/sw_bls12381/map_to_g1_test.go @@ -0,0 +1,147 @@ +package sw_bls12381 + +import ( + "fmt" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fp" + "github.com/consensys/gnark-crypto/ecc/bls12-381/hash_to_curve" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/math/emulated" + "github.com/consensys/gnark/test" +) + +// Test clear cofactor +type ClearCofactorCircuit struct { + Point G1Affine + Res G1Affine +} + +func (circuit *ClearCofactorCircuit) Define(api frontend.API) error { + g, err := NewG1(api) + if err != nil { + return err + } + clearedPoint := g.ClearCofactor(&circuit.Point) + g.AssertIsEqual(clearedPoint, &circuit.Res) + return nil +} + +func TestClearCofactor(t *testing.T) { + assert := test.NewAssert(t) + _, _, g1, _ := bls12381.Generators() + var g2 bls12381.G1Affine + g2.ClearCofactor(&g1) + witness := ClearCofactorCircuit{ + Point: NewG1Affine(g1), + Res: NewG1Affine(g2), + } + err := test.IsSolved(&ClearCofactorCircuit{}, &witness, ecc.BN254.ScalarField()) + assert.NoError(err) + +} + +// Test MapToCurve +type MapToCurveCircuit struct { + U emulated.Element[BaseField] + Res G1Affine +} + +func (circuit *MapToCurveCircuit) Define(api frontend.API) error { + g, err := NewG1(api) + if err != nil { + return err + } + + r, err := g.MapToCurve1(&circuit.U) + if err != nil { + return err + } + + g.AssertIsEqual(r, &circuit.Res) + + return nil +} + +func TestMapToCurve(t *testing.T) { + + assert := test.NewAssert(t) + var a fp.Element + a.SetRandom() + g := bls12381.MapToCurve1(&a) + + witness := MapToCurveCircuit{ + U: emulated.ValueOf[emulated.BLS12381Fp](a.String()), + Res: NewG1Affine(g), + } + err := test.IsSolved(&MapToCurveCircuit{}, &witness, ecc.BN254.ScalarField()) + assert.NoError(err) + +} + +// Test Map to G1 +type MapToG1Circuit struct { + A emulated.Element[BaseField] + R G1Affine +} + +func (circuit *MapToG1Circuit) Define(api frontend.API) error { + g, err := NewG1(api) + if err != nil { + return fmt.Errorf("new G1: %w", err) + } + res, err := g.MapToG1(&circuit.A) + if err != nil { + return err + } + + g.AssertIsEqual(res, &circuit.R) + + return nil +} + +func TestMapToG1(t *testing.T) { + + assert := test.NewAssert(t) + var a fp.Element + a.SetRandom() + g := bls12381.MapToG1(a) + + witness := MapToG1Circuit{ + A: emulated.ValueOf[emulated.BLS12381Fp](a.String()), + R: NewG1Affine(g), + } + err := test.IsSolved(&MapToG1Circuit{}, &witness, ecc.BN254.ScalarField()) + assert.NoError(err) +} + +type IsogenyG1Circuit struct { + In G1Affine + Res G1Affine +} + +func (c *IsogenyG1Circuit) Define(api frontend.API) error { + g, err := NewG1(api) + if err != nil { + return err + } + res := g.isogeny(&c.In) + g.AssertIsEqual(res, &c.Res) + return nil +} + +func TestIsogenyG1(t *testing.T) { + assert := test.NewAssert(t) + in, _ := randomG1G2Affines() + var res bls12381.G1Affine + res.Set(&in) + hash_to_curve.G1Isogeny(&res.X, &res.Y) + witness := IsogenyG1Circuit{ + In: NewG1Affine(in), + Res: NewG1Affine(res), + } + err := test.IsSolved(&IsogenyG1Circuit{}, &witness, ecc.BN254.ScalarField()) + assert.NoError(err) +} diff --git a/std/algebra/emulated/sw_bls12381/map_to_g2.go b/std/algebra/emulated/sw_bls12381/map_to_g2.go new file mode 100644 index 00000000..9c94b2fa --- /dev/null +++ b/std/algebra/emulated/sw_bls12381/map_to_g2.go @@ -0,0 +1,195 @@ +package sw_bls12381 + +import ( + "fmt" + + bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" + "github.com/consensys/gnark-crypto/ecc/bls12-381/hash_to_curve" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/emulated/fields_bls12381" +) + +func (g2 *G2) evalFixedPolynomial(monic bool, coefficients []bls12381.E2, x *fields_bls12381.E2) *fields_bls12381.E2 { + emuCoefficients := make([]*fields_bls12381.E2, len(coefficients)) + for i := 0; i < len(coefficients); i++ { + emuCoefficients[i] = &fields_bls12381.E2{ + A0: *g2.fp.NewElement(coefficients[i].A0), + A1: *g2.fp.NewElement(coefficients[i].A1), + } + } + var res *fields_bls12381.E2 + if monic { + res = g2.Add(emuCoefficients[len(emuCoefficients)-1], x) + } else { + res = emuCoefficients[len(emuCoefficients)-1] + } + + for i := len(emuCoefficients) - 2; i >= 0; i-- { + res = g2.Mul(res, x) + res = g2.Add(res, emuCoefficients[i]) + } + + return res +} + +func (g2 *G2) isogeny(p *G2Affine) *G2Affine { + isogenyMap := hash_to_curve.G2IsogenyMap() + ydenom := g2.evalFixedPolynomial(true, isogenyMap[3], &p.P.X) + xdenom := g2.evalFixedPolynomial(true, isogenyMap[1], &p.P.X) + y := g2.evalFixedPolynomial(false, isogenyMap[2], &p.P.X) + y = g2.Mul(y, &p.P.Y) + x := g2.evalFixedPolynomial(false, isogenyMap[0], &p.P.X) + x = g2.DivUnchecked(x, xdenom) + y = g2.DivUnchecked(y, ydenom) + return &G2Affine{P: g2AffP{X: *x, Y: *y}} +} + +func (g2 *G2) sgn0(x *fields_bls12381.E2) frontend.Variable { + // https://www.rfc-editor.org/rfc/rfc9380.html#name-the-sgn0-function case m=2 + x0Bits := g2.fp.ToBitsCanonical(&x.A0) + x1Bits := g2.fp.ToBitsCanonical(&x.A1) + + sign0 := x0Bits[0] // 1. sign_0 = x_0 mod 2 + zero0 := g2.fp.IsZero(&x.A0) // 2. zero_0 = x_0 == 0 + sign1 := x1Bits[0] // 3. sign_1 = x_1 mod 2 + sign := g2.api.Or(sign0, g2.api.And(zero0, sign1)) // 4. s = sign_0 OR (zero_0 AND sign_1) + return sign +} + +// sqrtRatio computes u/v and returns (isQR, y) where isQR indicates if the +// result is a quadratic residue. +func (g2 *G2) sqrtRatio(u, v *fields_bls12381.E2) (frontend.Variable, *fields_bls12381.E2, error) { + // Steps + // 1. extract the base values of u, v, then compute G2SqrtRatio with gnark-crypto + x, err := g2.fp.NewHint(g2SqrtRatioHint, 3, &u.A0, &u.A1, &v.A0, &v.A1) + if err != nil { + return nil, nil, fmt.Errorf("failed to calculate sqrtRatio with gnark-crypto: %w", err) + } + + b := g2.fp.IsZero(x[0]) + y := fields_bls12381.E2{A0: *x[1], A1: *x[2]} + + // 2. apply constraints + // b1 := {b = True AND y^2 * v = u} + g2.api.AssertIsBoolean(b) + y2 := g2.Ext2.Square(&y) + y2v := g2.Ext2.Mul(y2, v) + bY2vu := g2.Ext2.IsZero(g2.Ext2.Sub(y2v, u)) + b1 := g2.api.And(b, bY2vu) + + // b2 := {b = False AND y^2 * v = Z * u} + uZ := g2.Ext2.Mul(g2.sswuZ, u) + bY2vZu := g2.Ext2.IsZero(g2.Ext2.Sub(y2v, uZ)) + nb := g2.api.IsZero(b) + b2 := g2.api.And(nb, bY2vZu) + + cmp := g2.api.Xor(b1, b2) + g2.api.AssertIsEqual(cmp, 1) + + return b, &y, nil +} + +// ClearCofactor clears the cofactor of the point p in G2. +// +// See https://www.rfc-editor.org/rfc/rfc9380.html#name-cofactor-clearing-for-bls12 +func (g2 *G2) ClearCofactor(p *G2Affine) *G2Affine { + // Steps: + // 1. t1 = c1 * P + // c1 = -15132376222941642752 + t1 := g2.scalarMulBySeed(p) + // 2. t2 = psi(P) + t2 := g2.psi(p) + // 3. t3 = 2 * P + t3 := g2.double(p) + // 4. t3 = psi2(t3) + t3 = g2.psi2(t3) + // 5. t3 = t3 - t2 + t3 = g2.sub(t3, t2) + // 6. t2 = t1 + t2 + t2 = g2.AddUnified(t1, t2) + // 7. t2 = c1 * t2 + t2 = g2.scalarMulBySeed(t2) + // 8. t3 = t3 + t2 + t3 = g2.AddUnified(t3, t2) + // 9. t3 = t3 - t1 + t3 = g2.sub(t3, t1) + // 10. Q = t3 - P + Q := g2.sub(t3, p) + // 11. return Q + return Q +} + +func (g2 *G2) MapToCurve2(u *fields_bls12381.E2) (*G2Affine, error) { + // SSWU Steps: + // 1. tv1 = u^2 + tv1 := g2.Ext2.Square(u) + // 2. tv1 = Z * tv1 + tv1 = g2.Ext2.Mul(g2.sswuZ, tv1) + // 3. tv2 = tv1^2 + tv2 := g2.Ext2.Square(tv1) + // 4. tv2 = tv2 + tv1 + tv2 = g2.Ext2.Add(tv2, tv1) + // 5. tv3 = tv2 + 1 + tv3 := g2.Ext2.Add(tv2, g2.Ext2.One()) + // 6. tv3 = B * tv3 + tv3 = g2.Ext2.Mul(g2.sswuCoeffB, tv3) + // 7. tv4 = CMOV(Z, -tv2, tv2 != 0) + s1 := g2.Ext2.IsZero(tv2) + tv4 := g2.Ext2.Select(s1, g2.sswuZ, g2.Ext2.Neg(tv2)) + // 8. tv4 = A * tv4 + tv4 = g2.Ext2.Mul(g2.sswuCoeffA, tv4) + // 9. tv2 = tv3^2 + tv2 = g2.Ext2.Square(tv3) + // 10. tv6 = tv4^2 + tv6 := g2.Ext2.Square(tv4) + // 11. tv5 = A * tv6 + tv5 := g2.Ext2.Mul(g2.sswuCoeffA, tv6) + // 12. tv2 = tv2 + tv5 + tv2 = g2.Ext2.Add(tv2, tv5) + // 13. tv2 = tv2 * tv3 + tv2 = g2.Ext2.Mul(tv2, tv3) + // 14. tv6 = tv6 * tv4 + tv6 = g2.Ext2.Mul(tv6, tv4) + // 15. tv5 = B * tv6 + tv5 = g2.Ext2.Mul(g2.sswuCoeffB, tv6) + // 16. tv2 = tv2 + tv5 + tv2 = g2.Ext2.Add(tv2, tv5) + // 17. x = tv1 * tv3 + x := g2.Ext2.Mul(tv1, tv3) + // 18. (is_gx1_square, y1) = sqrt_ratio(tv2, tv6) + isGx1Square, y1, err := g2.sqrtRatio(tv2, tv6) + if err != nil { + return nil, fmt.Errorf("square ratio: %w", err) + } + // 19. y = tv1 * u + y := g2.Ext2.Mul(tv1, u) + // 20. y = y * y1 + y = g2.Ext2.Mul(y, y1) + // 21. x = CMOV(x, tv3, is_gx1_square) + x = g2.Ext2.Select(isGx1Square, tv3, x) + // 22. y = CMOV(y, y1, is_gx1_square) + y = g2.Ext2.Select(isGx1Square, y1, y) + // 23. e1 = sgn0(u) == sgn0(y) + sgn0U := g2.sgn0(u) + sgn0Y := g2.sgn0(y) + e1 := g2.api.Xor(sgn0U, sgn0Y) // we keep in mind that e1 = 1-(sgn0U == sgn0Y) as in gnark-crypto + // 24. y = CMOV(-y, y, e1) + yNeg := g2.Ext2.Neg(y) + y = g2.Ext2.Select(e1, yNeg, y) // contrary to gnark-crypto, if e1=1 we select yNeg and y otherwise + // 25. x = x / tv4 + x = g2.Ext2.DivUnchecked(x, tv4) + // 26. return (x, y) + return &G2Affine{ + P: g2AffP{X: *x, Y: *y}, + }, nil +} + +func (g2 *G2) MapToG2(u *fields_bls12381.E2) (*G2Affine, error) { + res, err := g2.MapToCurve2(u) + if err != nil { + return nil, fmt.Errorf("map to curve: %w", err) + } + z := g2.isogeny(res) + z = g2.ClearCofactor(z) + return z, nil +} diff --git a/std/algebra/emulated/sw_bls12381/map_to_g2_test.go b/std/algebra/emulated/sw_bls12381/map_to_g2_test.go new file mode 100644 index 00000000..3b66b3db --- /dev/null +++ b/std/algebra/emulated/sw_bls12381/map_to_g2_test.go @@ -0,0 +1,143 @@ +package sw_bls12381 + +import ( + "testing" + + "github.com/consensys/gnark-crypto/ecc" + bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" + "github.com/consensys/gnark-crypto/ecc/bls12-381/hash_to_curve" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/emulated/fields_bls12381" + "github.com/consensys/gnark/test" +) + +type TestG2IsogenyCircuit struct { + In G2Affine + Expected G2Affine +} + +func (c *TestG2IsogenyCircuit) Define(api frontend.API) error { + g2, err := NewG2(api) + if err != nil { + return err + } + res := g2.isogeny(&c.In) + g2.AssertIsEqual(res, &c.Expected) + return nil +} + +func TestG2Isogeny(t *testing.T) { + assert := test.NewAssert(t) + _, in := randomG1G2Affines() + var res bls12381.G2Affine + res.Set(&in) + hash_to_curve.G2Isogeny(&res.X, &res.Y) + assignment := TestG2IsogenyCircuit{ + In: NewG2Affine(in), + Expected: NewG2Affine(res), + } + err := test.IsSolved(&TestG2IsogenyCircuit{}, &assignment, ecc.BN254.ScalarField()) + assert.NoError(err) +} + +type clearCofactorCircuit struct { + In G2Affine + Res G2Affine +} + +func (c *clearCofactorCircuit) Define(api frontend.API) error { + g2, err := NewG2(api) + if err != nil { + return err + } + res := g2.ClearCofactor(&c.In) + g2.AssertIsEqual(res, &c.Res) + return nil +} + +func TestClearCofactorTestSolve(t *testing.T) { + assert := test.NewAssert(t) + _, in := randomG1G2Affines() + + inAffine := NewG2Affine(in) + + in.ClearCofactor(&in) + circuit := clearCofactorCircuit{ + In: inAffine, + Res: NewG2Affine(in), + } + witness := clearCofactorCircuit{ + In: inAffine, + Res: NewG2Affine(in), + } + err := test.IsSolved(&circuit, &witness, ecc.BN254.ScalarField()) + assert.NoError(err) +} + +type MapToCurve2Circuit struct { + In fields_bls12381.E2 + Expected G2Affine +} + +func (c *MapToCurve2Circuit) Define(api frontend.API) error { + g2, err := NewG2(api) + if err != nil { + return err + } + res, err := g2.MapToCurve2(&c.In) + if err != nil { + return err + } + g2.AssertIsEqual(res, &c.Expected) + return nil +} + +func TestMapToCurve2(t *testing.T) { + assert := test.NewAssert(t) + var e2 bls12381.E2 + e2.A0.SetRandom() + e2.A1.SetRandom() + + res := bls12381.MapToCurve2(&e2) + + assignment := MapToCurve2Circuit{ + In: fields_bls12381.FromE2(&e2), + Expected: NewG2Affine(res), + } + err := test.IsSolved(&MapToCurve2Circuit{}, &assignment, ecc.BN254.ScalarField()) + assert.NoError(err) +} + +type MapToG2Circuit struct { + In fields_bls12381.E2 + Expected G2Affine +} + +func (c *MapToG2Circuit) Define(api frontend.API) error { + g2, err := NewG2(api) + if err != nil { + return err + } + res, err := g2.MapToG2(&c.In) + if err != nil { + return err + } + g2.AssertIsEqual(res, &c.Expected) + return nil +} + +func TestMapToG2(t *testing.T) { + assert := test.NewAssert(t) + var e2 bls12381.E2 + e2.A0.SetRandom() + e2.A1.SetRandom() + + res := bls12381.MapToG2(e2) + + assignment := MapToG2Circuit{ + In: fields_bls12381.FromE2(&e2), + Expected: NewG2Affine(res), + } + err := test.IsSolved(&MapToG2Circuit{}, &assignment, ecc.BN254.ScalarField()) + assert.NoError(err) +} diff --git a/std/algebra/emulated/sw_bls12381/pairing.go b/std/algebra/emulated/sw_bls12381/pairing.go index 183bde4f..8ebb8386 100644 --- a/std/algebra/emulated/sw_bls12381/pairing.go +++ b/std/algebra/emulated/sw_bls12381/pairing.go @@ -21,7 +21,6 @@ type Pairing struct { curve *sw_emulated.Curve[BaseField, ScalarField] g2 *G2 g1 *G1 - bTwist *fields_bls12381.E2 } type baseEl = emulated.Element[BaseField] @@ -62,14 +61,14 @@ func NewPairing(api frontend.API) (*Pairing, error) { if err != nil { return nil, fmt.Errorf("new curve: %w", err) } - bTwist := fields_bls12381.E2{ - A0: emulated.ValueOf[BaseField]("4"), - A1: emulated.ValueOf[BaseField]("4"), - } g1, err := NewG1(api) if err != nil { return nil, fmt.Errorf("new G1 struct: %w", err) } + g2, err := NewG2(api) + if err != nil { + return nil, fmt.Errorf("new G2 struct: %w", err) + } return &Pairing{ api: api, Ext12: fields_bls12381.NewExt12(api), @@ -77,8 +76,7 @@ func NewPairing(api frontend.API) (*Pairing, error) { curveF: ba, curve: curve, g1: g1, - g2: NewG2(api), - bTwist: &bTwist, + g2: g2, }, nil } @@ -181,55 +179,177 @@ func (pr Pairing) PairingCheck(P []*G1Affine, Q []*G2Affine) error { return nil } +func (pr Pairing) IsEqual(x, y *GTEl) frontend.Variable { + return pr.Ext12.IsEqual(x, y) +} + func (pr Pairing) AssertIsEqual(x, y *GTEl) { pr.Ext12.AssertIsEqual(x, y) } -func (pr Pairing) AssertIsOnCurve(P *G1Affine) { - pr.curve.AssertIsOnCurve(P) +func (pr Pairing) MuxG2(sel frontend.Variable, inputs ...*G2Affine) *G2Affine { + if len(inputs) == 0 { + return nil + } + if len(inputs) == 1 { + pr.api.AssertIsEqual(sel, 0) + return inputs[0] + } + for i := 1; i < len(inputs); i++ { + if (inputs[0].Lines == nil) != (inputs[i].Lines == nil) { + panic("muxing points with and without precomputed lines") + } + } + var ret G2Affine + XA0 := make([]*emulated.Element[BaseField], len(inputs)) + XA1 := make([]*emulated.Element[BaseField], len(inputs)) + YA0 := make([]*emulated.Element[BaseField], len(inputs)) + YA1 := make([]*emulated.Element[BaseField], len(inputs)) + for i := range inputs { + XA0[i] = &inputs[i].P.X.A0 + XA1[i] = &inputs[i].P.X.A1 + YA0[i] = &inputs[i].P.Y.A0 + YA1[i] = &inputs[i].P.Y.A1 + } + ret.P.X.A0 = *pr.curveF.Mux(sel, XA0...) + ret.P.X.A1 = *pr.curveF.Mux(sel, XA1...) + ret.P.Y.A0 = *pr.curveF.Mux(sel, YA0...) + ret.P.Y.A1 = *pr.curveF.Mux(sel, YA1...) + + if inputs[0].Lines == nil { + return &ret + } + + // switch precomputed lines + ret.Lines = new(lineEvaluations) + for j := range inputs[0].Lines[0] { + lineR0A0 := make([]*emulated.Element[BaseField], len(inputs)) + lineR0A1 := make([]*emulated.Element[BaseField], len(inputs)) + lineR1A0 := make([]*emulated.Element[BaseField], len(inputs)) + lineR1A1 := make([]*emulated.Element[BaseField], len(inputs)) + for k := 0; k < 2; k++ { + for i := range inputs { + lineR0A0[i] = &inputs[i].Lines[k][j].R0.A0 + lineR0A1[i] = &inputs[i].Lines[k][j].R0.A1 + lineR1A0[i] = &inputs[i].Lines[k][j].R1.A0 + lineR1A1[i] = &inputs[i].Lines[k][j].R1.A1 + } + le := &lineEvaluation{ + R0: fields_bls12381.E2{ + A0: *pr.curveF.Mux(sel, lineR0A0...), + A1: *pr.curveF.Mux(sel, lineR0A1...), + }, + R1: fields_bls12381.E2{ + A0: *pr.curveF.Mux(sel, lineR1A0...), + A1: *pr.curveF.Mux(sel, lineR1A1...), + }, + } + ret.Lines[k][j] = le + } + } + + return &ret } -func (pr Pairing) AssertIsOnTwist(Q *G2Affine) { - // Twist: Y² == X³ + aX + b, where a=0 and b=4(1+u) - // (X,Y) ∈ {Y² == X³ + aX + b} U (0,0) - - // if Q=(0,0) we assign b=0 otherwise 4(1+u), and continue - selector := pr.api.And(pr.Ext2.IsZero(&Q.P.X), pr.Ext2.IsZero(&Q.P.Y)) - b := pr.Ext2.Select(selector, pr.Ext2.Zero(), pr.bTwist) - - left := pr.Ext2.Square(&Q.P.Y) - right := pr.Ext2.Square(&Q.P.X) - right = pr.Ext2.Mul(right, &Q.P.X) - right = pr.Ext2.Add(right, b) - pr.Ext2.AssertIsEqual(left, right) +func (pr Pairing) MuxGt(sel frontend.Variable, inputs ...*GTEl) *GTEl { + if len(inputs) == 0 { + return nil + } + if len(inputs) == 1 { + pr.api.AssertIsEqual(sel, 0) + return inputs[0] + } + var ret GTEl + A0s := make([]*emulated.Element[BaseField], len(inputs)) + A1s := make([]*emulated.Element[BaseField], len(inputs)) + A2s := make([]*emulated.Element[BaseField], len(inputs)) + A3s := make([]*emulated.Element[BaseField], len(inputs)) + A4s := make([]*emulated.Element[BaseField], len(inputs)) + A5s := make([]*emulated.Element[BaseField], len(inputs)) + A6s := make([]*emulated.Element[BaseField], len(inputs)) + A7s := make([]*emulated.Element[BaseField], len(inputs)) + A8s := make([]*emulated.Element[BaseField], len(inputs)) + A9s := make([]*emulated.Element[BaseField], len(inputs)) + A10s := make([]*emulated.Element[BaseField], len(inputs)) + A11s := make([]*emulated.Element[BaseField], len(inputs)) + for i := range inputs { + A0s[i] = &inputs[i].A0 + A1s[i] = &inputs[i].A1 + A2s[i] = &inputs[i].A2 + A3s[i] = &inputs[i].A3 + A4s[i] = &inputs[i].A4 + A5s[i] = &inputs[i].A5 + A6s[i] = &inputs[i].A6 + A7s[i] = &inputs[i].A7 + A8s[i] = &inputs[i].A8 + A9s[i] = &inputs[i].A9 + A10s[i] = &inputs[i].A10 + A11s[i] = &inputs[i].A11 + } + ret.A0 = *pr.curveF.Mux(sel, A0s...) + ret.A1 = *pr.curveF.Mux(sel, A1s...) + ret.A2 = *pr.curveF.Mux(sel, A2s...) + ret.A3 = *pr.curveF.Mux(sel, A3s...) + ret.A4 = *pr.curveF.Mux(sel, A4s...) + ret.A5 = *pr.curveF.Mux(sel, A5s...) + ret.A6 = *pr.curveF.Mux(sel, A6s...) + ret.A7 = *pr.curveF.Mux(sel, A7s...) + ret.A8 = *pr.curveF.Mux(sel, A8s...) + ret.A9 = *pr.curveF.Mux(sel, A9s...) + ret.A10 = *pr.curveF.Mux(sel, A10s...) + ret.A11 = *pr.curveF.Mux(sel, A11s...) + return &ret +} + +// IsOnCurve returns a boolean indicating if the G1 point is in the curve. +func (pr Pairing) IsOnCurve(P *G1Affine) frontend.Variable { + left, right := pr.g1.computeCurveEquation(P) + diff := pr.curveF.Sub(left, right) + return pr.curveF.IsZero(diff) } func (pr Pairing) AssertIsOnG1(P *G1Affine) { - // 1- Check P is on the curve - pr.AssertIsOnCurve(P) + pr.g1.AssertIsOnG1(P) +} - // 2- Check P has the right subgroup order - // [x²]ϕ(P) +// IsOnG1 returns a boolean indicating if the G1 point is on the curve and in +// the prime subgroup. +func (pr Pairing) IsOnG1(P *G1Affine) frontend.Variable { + // 1 - is Q on curve + isOnCurve := pr.IsOnCurve(P) + // 2 - is Q in the subgroup phiP := pr.g1.phi(P) _P := pr.g1.scalarMulBySeedSquare(phiP) _P = pr.curve.Neg(_P) + isInSubgroup := pr.g1.IsEqual(_P, phiP) + return pr.api.And(isOnCurve, isInSubgroup) +} + +func (pr Pairing) AssertIsOnTwist(Q *G2Affine) { + pr.g2.AssertIsOnTwist(Q) +} - // [r]Q == 0 <==> P = -[x²]ϕ(P) - pr.curve.AssertIsEqual(_P, P) +// IsOnTwist returns a boolean indicating if the G2 point is in the twist. +func (pr Pairing) IsOnTwist(Q *G2Affine) frontend.Variable { + left, right := pr.g2.computeTwistEquation(Q) + diff := pr.Ext2.Sub(left, right) + return pr.Ext2.IsZero(diff) } func (pr Pairing) AssertIsOnG2(Q *G2Affine) { - // 1- Check Q is on the curve - pr.AssertIsOnTwist(Q) + pr.g2.AssertIsOnG2(Q) +} - // 2- Check Q has the right subgroup order - // [x₀]Q +// IsOnG2 returns a boolean indicating if the G2 point is on the curve and in +// the prime subgroup. +func (pr Pairing) IsOnG2(Q *G2Affine) frontend.Variable { + // 1 - is Q on curve + isOnCurve := pr.IsOnTwist(Q) + // 2 - is Q in the subgroup xQ := pr.g2.scalarMulBySeed(Q) - // ψ(Q) psiQ := pr.g2.psi(Q) - - // [r]Q == 0 <==> ψ(Q) == [x₀]Q - pr.g2.AssertIsEqual(xQ, psiQ) + isInSubgroup := pr.g2.IsEqual(xQ, psiQ) + return pr.api.And(isOnCurve, isInSubgroup) } // loopCounter = seed in binary @@ -589,22 +709,122 @@ func (pr Pairing) tripleStep(p1 *g2AffP) (*g2AffP, *lineEvaluation, *lineEvaluat return &res, &line1, &line2 } -// tangentCompute computes the tangent line to p1, but does not compute [2]p1. -func (pr Pairing) tangentCompute(p1 *g2AffP) *lineEvaluation { +// MillerLoopAndMul computes the Miller loop between P and Q +// and multiplies it in 𝔽p¹² by previous. +// +// This method is needed for evmprecompiles/ecpair. +func (pr Pairing) MillerLoopAndMul(P *G1Affine, Q *G2Affine, previous *GTEl) (*GTEl, error) { + res, err := pr.MillerLoop([]*G1Affine{P}, []*G2Affine{Q}) + if err != nil { + return nil, fmt.Errorf("miller loop: %w", err) + } + res = pr.Ext12.Conjugate(res) + res = pr.Ext12.Mul(res, previous) + return res, err +} - // λ = 3x²/2y - n := pr.Ext2.Square(&p1.X) - three := big.NewInt(3) - n = pr.Ext2.MulByConstElement(n, three) - d := pr.Ext2.Double(&p1.Y) - λ := pr.Ext2.DivUnchecked(n, d) +// AssertMillerLoopAndFinalExpIsOne computes the Miller loop between P and Q, +// multiplies it in 𝔽p¹² by previous and checks that the result lies in the +// same equivalence class as the reduced pairing purported to be 1. This check +// replaces the final exponentiation step in-circuit and follows Section 4 of +// [On Proving Pairings] paper by A. Novakovic and L. Eagen. +// +// This method is needed for evmprecompiles/ecpair. +// +// [On Proving Pairings]: https://eprint.iacr.org/2024/640.pdf +func (pr Pairing) AssertMillerLoopAndFinalExpIsOne(P *G1Affine, Q *G2Affine, previous *GTEl) { + t2 := pr.millerLoopAndFinalExpResult(P, Q, previous) + pr.AssertIsEqual(t2, pr.Ext12.One()) +} - var line lineEvaluation - mone := pr.curveF.NewElement(-1) - line.R0 = *λ - line.R1.A0 = *pr.curveF.Eval([][]*baseEl{{&λ.A0, &p1.X.A0}, {mone, &λ.A1, &p1.X.A1}, {mone, &p1.Y.A0}}, []int{1, 1, 1}) - line.R1.A1 = *pr.curveF.Eval([][]*baseEl{{&λ.A0, &p1.X.A1}, {&λ.A1, &p1.X.A0}, {mone, &p1.Y.A1}}, []int{1, 1, 1}) +// millerLoopAndFinalExpResult computes the Miller loop between P and Q, +// multiplies it in 𝔽p¹² by previous and returns the result. +func (pr Pairing) millerLoopAndFinalExpResult(P *G1Affine, Q *G2Affine, previous *GTEl) *GTEl { + tower := pr.ToTower(previous) + + // hint the non-residue witness + hint, err := pr.curveF.NewHint(millerLoopAndCheckFinalExpHint, 18, &P.X, &P.Y, &Q.P.X.A0, &Q.P.X.A1, &Q.P.Y.A0, &Q.P.Y.A1, tower[0], tower[1], tower[2], tower[3], tower[4], tower[5], tower[6], tower[7], tower[8], tower[9], tower[10], tower[11]) + if err != nil { + // err is non-nil only for invalid number of inputs + panic(err) + } + residueWitnessInv := pr.Ext12.FromTower([12]*baseEl{hint[0], hint[1], hint[2], hint[3], hint[4], hint[5], hint[6], hint[7], hint[8], hint[9], hint[10], hint[11]}) + // constrain scalingFactor to be in Fp6 + // that is: a100=a101=a110=a111=a120=a121=0 + // or + // A0 = a000 - a001 + // A1 = 0 + // A2 = a010 - a011 + // A3 = 0 + // A4 = a020 - a021 + // A5 = 0 + // A6 = a001 + // A7 = 0 + // A8 = a011 + // A9 = 0 + // A10 = a021 + // A11 = 0 + scalingFactor := GTEl{ + A0: *pr.curveF.Sub(hint[12], hint[13]), + A1: *pr.curveF.Zero(), + A2: *pr.curveF.Sub(hint[14], hint[15]), + A3: *pr.curveF.Zero(), + A4: *pr.curveF.Sub(hint[16], hint[17]), + A5: *pr.curveF.Zero(), + A6: *hint[13], + A7: *pr.curveF.Zero(), + A8: *hint[15], + A9: *pr.curveF.Zero(), + A10: *hint[17], + A11: *pr.curveF.Zero(), + } + + if Q.Lines == nil { + Qlines := pr.computeLines(&Q.P) + Q.Lines = &Qlines + } + lines := *Q.Lines + + res, err := pr.millerLoopLines( + []*G1Affine{P}, + []lineEvaluations{lines}, + residueWitnessInv, + false, + ) + if err != nil { + return nil + } + res = pr.Ext12.Conjugate(res) + + // multiply by previous multi-Miller function + res = pr.Ext12.Mul(res, previous) - return &line + // Check that: MillerLoop(P,Q) * scalingFactor * residueWitnessInv^(p-x₀) == 1 + // where u=-0xd201000000010000 is the BLS12-381 seed, and residueWitnessInv, + // scalingFactor from the hint. + // Note that res is already MillerLoop(P,Q) * residueWitnessInv^{-x₀} since + // we initialized the Miller loop accumulator with residueWitnessInv. + // So we only need to check that: + // res * scalingFactor * residueWitnessInv^p == 1 + res = pr.Ext12.Mul(res, &scalingFactor) + t0 := pr.Frobenius(residueWitnessInv) + res = pr.Ext12.Mul(res, t0) + + return res + +} + +// IsMillerLoopAndFinalExpOne computes the Miller loop between P and Q, +// multiplies it in 𝔽p¹² by previous and returns a boolean indicating if +// the result lies in the same equivalence class as the reduced pairing +// purported to be 1. +// +// This method is needed for evmprecompiles/ecpair. +// +// [On Proving Pairings]: https://eprint.iacr.org/2024/640.pdf +func (pr Pairing) IsMillerLoopAndFinalExpOne(P *G1Affine, Q *G2Affine, previous *GTEl) frontend.Variable { + t2 := pr.millerLoopAndFinalExpResult(P, Q, previous) + res := pr.IsEqual(t2, pr.Ext12.One()) + return res } diff --git a/std/algebra/emulated/sw_bls12381/pairing_test.go b/std/algebra/emulated/sw_bls12381/pairing_test.go index 7020c324..6c69e12a 100644 --- a/std/algebra/emulated/sw_bls12381/pairing_test.go +++ b/std/algebra/emulated/sw_bls12381/pairing_test.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/rand" "fmt" + "math/big" "testing" "github.com/consensys/gnark-crypto/ecc" @@ -302,6 +303,99 @@ func TestGroupMembershipSolve(t *testing.T) { assert.NoError(err) } +type MuxesCircuits struct { + InG2 []G2Affine + InGt []GTEl + SelG2 frontend.Variable + SelGt frontend.Variable + ExpectedG2 G2Affine + ExpectedGt GTEl +} + +func (c *MuxesCircuits) Define(api frontend.API) error { + g2api, err := NewG2(api) + if err != nil { + return fmt.Errorf("new G2 struct: %w", err) + } + pairing, err := NewPairing(api) + if err != nil { + return fmt.Errorf("new pairing: %w", err) + } + var inG2 []*G2Affine + for i := range c.InG2 { + inG2 = append(inG2, &c.InG2[i]) + } + var inGt []*GTEl + for i := range c.InGt { + inGt = append(inGt, &c.InGt[i]) + } + g2 := pairing.MuxG2(c.SelG2, inG2...) + gt := pairing.MuxGt(c.SelGt, inGt...) + if len(c.InG2) == 0 { + if g2 != nil { + return fmt.Errorf("mux G2: expected nil, got %v", g2) + } + } else { + g2api.AssertIsEqual(g2, &c.ExpectedG2) + } + if len(c.InGt) == 0 { + if gt != nil { + return fmt.Errorf("mux Gt: expected nil, got %v", gt) + } + } else { + pairing.AssertIsEqual(gt, &c.ExpectedGt) + } + return nil +} + +func TestPairingMuxes(t *testing.T) { + assert := test.NewAssert(t) + var err error + for _, nbPairs := range []int{0, 1, 2, 3, 4, 5} { + assert.Run(func(assert *test.Assert) { + g2s := make([]bls12381.G2Affine, nbPairs) + gts := make([]bls12381.GT, nbPairs) + var p bls12381.G1Affine + witG2s := make([]G2Affine, nbPairs) + witGts := make([]GTEl, nbPairs) + for i := range nbPairs { + p, g2s[i] = randomG1G2Affines() + gts[i], err = bls12381.Pair([]bls12381.G1Affine{p}, []bls12381.G2Affine{g2s[i]}) + assert.NoError(err) + witG2s[i] = NewG2Affine(g2s[i]) + witGts[i] = NewGTEl(gts[i]) + } + circuit := MuxesCircuits{InG2: make([]G2Affine, nbPairs), InGt: make([]GTEl, nbPairs)} + var witness MuxesCircuits + if nbPairs > 0 { + selG2, err := rand.Int(rand.Reader, big.NewInt(int64(nbPairs))) + assert.NoError(err) + selGt, err := rand.Int(rand.Reader, big.NewInt(int64(nbPairs))) + assert.NoError(err) + expectedG2 := witG2s[selG2.Int64()] + expectedGt := witGts[selGt.Int64()] + witness = MuxesCircuits{ + InG2: witG2s, + InGt: witGts, + SelG2: selG2, + SelGt: selGt, + ExpectedG2: expectedG2, + ExpectedGt: expectedGt, + } + } else { + witness = MuxesCircuits{ + InG2: witG2s, + InGt: witGts, + SelG2: big.NewInt(0), + SelGt: big.NewInt(0), + } + } + err = test.IsSolved(&circuit, &witness, ecc.BN254.ScalarField()) + assert.NoError(err) + }, fmt.Sprintf("nbPairs=%d", nbPairs)) + } +} + // bench func BenchmarkPairing(b *testing.B) { // e(a,2b) * e(-2a,b) == 1 diff --git a/std/algebra/emulated/sw_bls12381/precomputations.go b/std/algebra/emulated/sw_bls12381/precomputations.go index a07efc79..5049816f 100644 --- a/std/algebra/emulated/sw_bls12381/precomputations.go +++ b/std/algebra/emulated/sw_bls12381/precomputations.go @@ -31,17 +31,29 @@ func precomputeLines(Q bls12381.G2Affine) lineEvaluations { func (p *Pairing) computeLines(Q *g2AffP) lineEvaluations { + // check Q is on curve + Qaff := G2Affine{P: *Q, Lines: nil} + p.IsOnTwist(&Qaff) + var cLines lineEvaluations Qacc := Q n := len(loopCounter) Qacc, cLines[0][n-2], cLines[1][n-2] = p.tripleStep(Qacc) - for i := n - 3; i >= 1; i-- { + for i := n - 3; i >= 0; i-- { if loopCounter[i] == 0 { Qacc, cLines[0][i] = p.doubleStep(Qacc) } else { Qacc, cLines[0][i], cLines[1][i] = p.doubleAndAddStep(Qacc, Q) } } - cLines[0][0] = p.tangentCompute(Qacc) + + // Check that Q is on G2 subgroup: + // [r]Q == 0 <==> ψ(Q) == [x₀]Q + // This test is equivalent to [AssertIsOnG2]. + // + // At this point Qacc = [x₀]Q. + psiQ := p.g2.psi(&Qaff) + p.g2.AssertIsEqual(p.g2.neg(&G2Affine{P: *Qacc, Lines: nil}), psiQ) + return cLines } diff --git a/std/algebra/emulated/sw_bn254/g2.go b/std/algebra/emulated/sw_bn254/g2.go index c7017d6f..c1ec2a10 100644 --- a/std/algebra/emulated/sw_bn254/g2.go +++ b/std/algebra/emulated/sw_bn254/g2.go @@ -1,6 +1,7 @@ package sw_bn254 import ( + "fmt" "math/big" "github.com/consensys/gnark-crypto/ecc/bn254" @@ -40,29 +41,28 @@ func newG2AffP(v bn254.G2Affine) g2AffP { } } -func NewG2(api frontend.API) *G2 { - fp, err := emulated.NewField[emulated.BN254Fp](api) +func NewG2(api frontend.API) (*G2, error) { + fp, err := emulated.NewField[BaseField](api) if err != nil { - // TODO: we start returning errors when generifying - panic(err) + return nil, fmt.Errorf("new base api: %w", err) } - w := emulated.ValueOf[BaseField]("21888242871839275220042445260109153167277707414472061641714758635765020556616") + w := fp.NewElement("21888242871839275220042445260109153167277707414472061641714758635765020556616") u := fields_bn254.E2{ - A0: emulated.ValueOf[BaseField]("21575463638280843010398324269430826099269044274347216827212613867836435027261"), - A1: emulated.ValueOf[BaseField]("10307601595873709700152284273816112264069230130616436755625194854815875713954"), + A0: *fp.NewElement("21575463638280843010398324269430826099269044274347216827212613867836435027261"), + A1: *fp.NewElement("10307601595873709700152284273816112264069230130616436755625194854815875713954"), } v := fields_bn254.E2{ - A0: emulated.ValueOf[BaseField]("2821565182194536844548159561693502659359617185244120367078079554186484126554"), - A1: emulated.ValueOf[BaseField]("3505843767911556378687030309984248845540243509899259641013678093033130930403"), + A0: *fp.NewElement("2821565182194536844548159561693502659359617185244120367078079554186484126554"), + A1: *fp.NewElement("3505843767911556378687030309984248845540243509899259641013678093033130930403"), } return &G2{ api: api, fp: fp, Ext2: fields_bn254.NewExt2(api), - w: &w, + w: w, u: &u, v: &v, - } + }, nil } func NewG2Affine(v bn254.G2Affine) G2Affine { diff --git a/std/algebra/emulated/sw_bn254/g2_test.go b/std/algebra/emulated/sw_bn254/g2_test.go index 4d487962..812e21e0 100644 --- a/std/algebra/emulated/sw_bn254/g2_test.go +++ b/std/algebra/emulated/sw_bn254/g2_test.go @@ -16,7 +16,10 @@ type addG2Circuit struct { } func (c *addG2Circuit) Define(api frontend.API) error { - g2 := NewG2(api) + g2, err := NewG2(api) + if err != nil { + panic(err) + } res := g2.add(&c.In1, &c.In2) g2.AssertIsEqual(res, &c.Res) return nil @@ -43,7 +46,10 @@ type doubleG2Circuit struct { } func (c *doubleG2Circuit) Define(api frontend.API) error { - g2 := NewG2(api) + g2, err := NewG2(api) + if err != nil { + panic(err) + } res := g2.double(&c.In1) g2.AssertIsEqual(res, &c.Res) return nil @@ -71,7 +77,10 @@ type doubleAndAddG2Circuit struct { } func (c *doubleAndAddG2Circuit) Define(api frontend.API) error { - g2 := NewG2(api) + g2, err := NewG2(api) + if err != nil { + panic(err) + } res := g2.doubleAndAdd(&c.In1, &c.In2) g2.AssertIsEqual(res, &c.Res) return nil @@ -99,7 +108,10 @@ type scalarMulG2BySeedCircuit struct { } func (c *scalarMulG2BySeedCircuit) Define(api frontend.API) error { - g2 := NewG2(api) + g2, err := NewG2(api) + if err != nil { + panic(err) + } res := g2.scalarMulBySeed(&c.In1) g2.AssertIsEqual(res, &c.Res) return nil @@ -124,7 +136,10 @@ type endomorphismG2Circuit struct { } func (c *endomorphismG2Circuit) Define(api frontend.API) error { - g2 := NewG2(api) + g2, err := NewG2(api) + if err != nil { + panic(err) + } res1 := g2.phi(&c.In1) res2 := g2.psi(&c.In1) res2 = g2.psi(res2) diff --git a/std/algebra/emulated/sw_bn254/pairing.go b/std/algebra/emulated/sw_bn254/pairing.go index 4e66488b..c919923b 100644 --- a/std/algebra/emulated/sw_bn254/pairing.go +++ b/std/algebra/emulated/sw_bn254/pairing.go @@ -67,8 +67,12 @@ func NewPairing(api frontend.API) (*Pairing, error) { return nil, fmt.Errorf("new curve: %w", err) } bTwist := fields_bn254.E2{ - A0: emulated.ValueOf[BaseField]("19485874751759354771024239261021720505790618469301721065564631296452457478373"), - A1: emulated.ValueOf[BaseField]("266929791119991161246907387137283842545076965332900288569378510910307636690"), + A0: *ba.NewElement("19485874751759354771024239261021720505790618469301721065564631296452457478373"), + A1: *ba.NewElement("266929791119991161246907387137283842545076965332900288569378510910307636690"), + } + g2, err := NewG2(api) + if err != nil { + return nil, fmt.Errorf("new g2: %w", err) } return &Pairing{ api: api, @@ -76,7 +80,7 @@ func NewPairing(api frontend.API) (*Pairing, error) { Ext2: fields_bn254.NewExt2(api), curveF: ba, curve: curve, - g2: NewG2(api), + g2: g2, bTwist: &bTwist, }, nil } @@ -313,6 +317,120 @@ func (pr Pairing) AssertIsOnCurve(P *G1Affine) { pr.curve.AssertIsOnCurve(P) } +func (pr Pairing) MuxG2(sel frontend.Variable, inputs ...*G2Affine) *G2Affine { + if len(inputs) == 0 { + return nil + } + if len(inputs) == 1 { + pr.api.AssertIsEqual(sel, 0) + return inputs[0] + } + for i := 1; i < len(inputs); i++ { + if (inputs[0].Lines == nil) != (inputs[i].Lines == nil) { + panic("muxing points with and without precomputed lines") + } + } + var ret G2Affine + XA0 := make([]*emulated.Element[BaseField], len(inputs)) + XA1 := make([]*emulated.Element[BaseField], len(inputs)) + YA0 := make([]*emulated.Element[BaseField], len(inputs)) + YA1 := make([]*emulated.Element[BaseField], len(inputs)) + for i := range inputs { + XA0[i] = &inputs[i].P.X.A0 + XA1[i] = &inputs[i].P.X.A1 + YA0[i] = &inputs[i].P.Y.A0 + YA1[i] = &inputs[i].P.Y.A1 + } + ret.P.X.A0 = *pr.curveF.Mux(sel, XA0...) + ret.P.X.A1 = *pr.curveF.Mux(sel, XA1...) + ret.P.Y.A0 = *pr.curveF.Mux(sel, YA0...) + ret.P.Y.A1 = *pr.curveF.Mux(sel, YA1...) + + if inputs[0].Lines == nil { + return &ret + } + + // switch precomputed lines + ret.Lines = new(lineEvaluations) + for j := range inputs[0].Lines[0] { + lineR0A0 := make([]*emulated.Element[BaseField], len(inputs)) + lineR0A1 := make([]*emulated.Element[BaseField], len(inputs)) + lineR1A0 := make([]*emulated.Element[BaseField], len(inputs)) + lineR1A1 := make([]*emulated.Element[BaseField], len(inputs)) + for k := 0; k < 2; k++ { + for i := range inputs { + lineR0A0[i] = &inputs[i].Lines[k][j].R0.A0 + lineR0A1[i] = &inputs[i].Lines[k][j].R0.A1 + lineR1A0[i] = &inputs[i].Lines[k][j].R1.A0 + lineR1A1[i] = &inputs[i].Lines[k][j].R1.A1 + } + le := &lineEvaluation{ + R0: fields_bn254.E2{ + A0: *pr.curveF.Mux(sel, lineR0A0...), + A1: *pr.curveF.Mux(sel, lineR0A1...), + }, + R1: fields_bn254.E2{ + A0: *pr.curveF.Mux(sel, lineR1A0...), + A1: *pr.curveF.Mux(sel, lineR1A1...), + }, + } + ret.Lines[k][j] = le + } + } + + return &ret +} + +func (pr Pairing) MuxGt(sel frontend.Variable, inputs ...*GTEl) *GTEl { + if len(inputs) == 0 { + return nil + } + if len(inputs) == 1 { + pr.api.AssertIsEqual(sel, 0) + return inputs[0] + } + var ret GTEl + A0s := make([]*emulated.Element[BaseField], len(inputs)) + A1s := make([]*emulated.Element[BaseField], len(inputs)) + A2s := make([]*emulated.Element[BaseField], len(inputs)) + A3s := make([]*emulated.Element[BaseField], len(inputs)) + A4s := make([]*emulated.Element[BaseField], len(inputs)) + A5s := make([]*emulated.Element[BaseField], len(inputs)) + A6s := make([]*emulated.Element[BaseField], len(inputs)) + A7s := make([]*emulated.Element[BaseField], len(inputs)) + A8s := make([]*emulated.Element[BaseField], len(inputs)) + A9s := make([]*emulated.Element[BaseField], len(inputs)) + A10s := make([]*emulated.Element[BaseField], len(inputs)) + A11s := make([]*emulated.Element[BaseField], len(inputs)) + for i := range inputs { + A0s[i] = &inputs[i].A0 + A1s[i] = &inputs[i].A1 + A2s[i] = &inputs[i].A2 + A3s[i] = &inputs[i].A3 + A4s[i] = &inputs[i].A4 + A5s[i] = &inputs[i].A5 + A6s[i] = &inputs[i].A6 + A7s[i] = &inputs[i].A7 + A8s[i] = &inputs[i].A8 + A9s[i] = &inputs[i].A9 + A10s[i] = &inputs[i].A10 + A11s[i] = &inputs[i].A11 + } + ret.A0 = *pr.curveF.Mux(sel, A0s...) + ret.A1 = *pr.curveF.Mux(sel, A1s...) + ret.A2 = *pr.curveF.Mux(sel, A2s...) + ret.A3 = *pr.curveF.Mux(sel, A3s...) + ret.A4 = *pr.curveF.Mux(sel, A4s...) + ret.A5 = *pr.curveF.Mux(sel, A5s...) + ret.A6 = *pr.curveF.Mux(sel, A6s...) + ret.A7 = *pr.curveF.Mux(sel, A7s...) + ret.A8 = *pr.curveF.Mux(sel, A8s...) + ret.A9 = *pr.curveF.Mux(sel, A9s...) + ret.A10 = *pr.curveF.Mux(sel, A10s...) + ret.A11 = *pr.curveF.Mux(sel, A11s...) + return &ret +} + func (pr Pairing) computeTwistEquation(Q *G2Affine) (left, right *fields_bn254.E2) { // Twist: Y² == X³ + aX + b, where a=0 and b=3/(9+u) // (X,Y) ∈ {Y² == X³ + aX + b} U (0,0) @@ -376,9 +494,8 @@ func (pr Pairing) AssertIsOnG2(Q *G2Affine) { pr.g2.AssertIsEqual(Q, _Q) } -// IsOnG2 returns a boolean indicating if the G2 point is in the subgroup. The -// method assumes that the point is already on the curve. Call -// [Pairing.AssertIsOnTwist] before to ensure point is on the curve. +// IsOnG2 returns a boolean indicating if the G2 point is on the curve and in +// the subgroup. func (pr Pairing) IsOnG2(Q *G2Affine) frontend.Variable { // 1 - is Q on curve isOnCurve := pr.IsOnTwist(Q) diff --git a/std/algebra/emulated/sw_bn254/pairing_test.go b/std/algebra/emulated/sw_bn254/pairing_test.go index 70e97c97..345a9514 100644 --- a/std/algebra/emulated/sw_bn254/pairing_test.go +++ b/std/algebra/emulated/sw_bn254/pairing_test.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/rand" "fmt" + "math/big" "testing" "github.com/consensys/gnark-crypto/ecc" @@ -467,6 +468,99 @@ func TestIsMillerLoopAndFinalExpCircuitTestSolve(t *testing.T) { assert.NoError(err) } +type MuxesCircuits struct { + InG2 []G2Affine + InGt []GTEl + SelG2 frontend.Variable + SelGt frontend.Variable + ExpectedG2 G2Affine + ExpectedGt GTEl +} + +func (c *MuxesCircuits) Define(api frontend.API) error { + g2api, err := NewG2(api) + if err != nil { + return fmt.Errorf("new G2: %w", err) + } + pairing, err := NewPairing(api) + if err != nil { + return fmt.Errorf("new pairing: %w", err) + } + var inG2 []*G2Affine + for i := range c.InG2 { + inG2 = append(inG2, &c.InG2[i]) + } + var inGt []*GTEl + for i := range c.InGt { + inGt = append(inGt, &c.InGt[i]) + } + g2 := pairing.MuxG2(c.SelG2, inG2...) + gt := pairing.MuxGt(c.SelGt, inGt...) + if len(c.InG2) == 0 { + if g2 != nil { + return fmt.Errorf("mux G2: expected nil, got %v", g2) + } + } else { + g2api.AssertIsEqual(g2, &c.ExpectedG2) + } + if len(c.InGt) == 0 { + if gt != nil { + return fmt.Errorf("mux Gt: expected nil, got %v", gt) + } + } else { + pairing.AssertIsEqual(gt, &c.ExpectedGt) + } + return nil +} + +func TestPairingMuxes(t *testing.T) { + assert := test.NewAssert(t) + var err error + for _, nbPairs := range []int{0, 1, 2, 3, 4, 5} { + assert.Run(func(assert *test.Assert) { + g2s := make([]bn254.G2Affine, nbPairs) + gts := make([]bn254.GT, nbPairs) + var p bn254.G1Affine + witG2s := make([]G2Affine, nbPairs) + witGts := make([]GTEl, nbPairs) + for i := range nbPairs { + p, g2s[i] = randomG1G2Affines() + gts[i], err = bn254.Pair([]bn254.G1Affine{p}, []bn254.G2Affine{g2s[i]}) + assert.NoError(err) + witG2s[i] = NewG2Affine(g2s[i]) + witGts[i] = NewGTEl(gts[i]) + } + circuit := MuxesCircuits{InG2: make([]G2Affine, nbPairs), InGt: make([]GTEl, nbPairs)} + var witness MuxesCircuits + if nbPairs > 0 { + selG2, err := rand.Int(rand.Reader, big.NewInt(int64(nbPairs))) + assert.NoError(err) + selGt, err := rand.Int(rand.Reader, big.NewInt(int64(nbPairs))) + assert.NoError(err) + expectedG2 := witG2s[selG2.Int64()] + expectedGt := witGts[selGt.Int64()] + witness = MuxesCircuits{ + InG2: witG2s, + InGt: witGts, + SelG2: selG2, + SelGt: selGt, + ExpectedG2: expectedG2, + ExpectedGt: expectedGt, + } + } else { + witness = MuxesCircuits{ + InG2: witG2s, + InGt: witGts, + SelG2: big.NewInt(0), + SelGt: big.NewInt(0), + } + } + err = test.IsSolved(&circuit, &witness, ecc.BN254.ScalarField()) + assert.NoError(err) + }, fmt.Sprintf("nbPairs=%d", nbPairs)) + } +} + // bench func BenchmarkPairing(b *testing.B) { // e(a,2b) * e(-2a,b) == 1 diff --git a/std/algebra/emulated/sw_bw6761/g1.go b/std/algebra/emulated/sw_bw6761/g1.go index 0c97334a..efd0f993 100644 --- a/std/algebra/emulated/sw_bw6761/g1.go +++ b/std/algebra/emulated/sw_bw6761/g1.go @@ -48,10 +48,10 @@ func NewG1(api frontend.API) (*G1, error) { if err != nil { return nil, fmt.Errorf("new base api: %w", err) } - w := emulated.ValueOf[BaseField]("1968985824090209297278610739700577151397666382303825728450741611566800370218827257750865013421937292370006175842381275743914023380727582819905021229583192207421122272650305267822868639090213645505120388400344940985710520836292650") + w := ba.NewElement("1968985824090209297278610739700577151397666382303825728450741611566800370218827257750865013421937292370006175842381275743914023380727582819905021229583192207421122272650305267822868639090213645505120388400344940985710520836292650") return &G1{ curveF: ba, - w: &w, + w: w, }, nil } diff --git a/std/algebra/emulated/sw_bw6761/g2.go b/std/algebra/emulated/sw_bw6761/g2.go index 0709eb5d..08f19223 100644 --- a/std/algebra/emulated/sw_bw6761/g2.go +++ b/std/algebra/emulated/sw_bw6761/g2.go @@ -68,10 +68,10 @@ func NewG2(api frontend.API) (*G2, error) { if err != nil { return nil, fmt.Errorf("new base api: %w", err) } - w := emulated.ValueOf[BaseField]("4922464560225523242118178942575080391082002530232324381063048548642823052024664478336818169867474395270858391911405337707247735739826664939444490469542109391530482826728203582549674992333383150446779312029624171857054392282775648") + w := ba.NewElement("4922464560225523242118178942575080391082002530232324381063048548642823052024664478336818169867474395270858391911405337707247735739826664939444490469542109391530482826728203582549674992333383150446779312029624171857054392282775648") return &G2{ curveF: ba, - w: &w, + w: w, }, nil } diff --git a/std/algebra/emulated/sw_bw6761/pairing.go b/std/algebra/emulated/sw_bw6761/pairing.go index 7ca01b1b..70fcca71 100644 --- a/std/algebra/emulated/sw_bw6761/pairing.go +++ b/std/algebra/emulated/sw_bw6761/pairing.go @@ -15,10 +15,11 @@ import ( type Pairing struct { api frontend.API *fields_bw6761.Ext6 - curveF *emulated.Field[BaseField] - curve *sw_emulated.Curve[BaseField, ScalarField] - g1 *G1 - g2 *G2 + curveF *emulated.Field[BaseField] + curve *sw_emulated.Curve[BaseField, ScalarField] + g1 *G1 + g2 *G2 + thirdRootOne *emulated.Element[BaseField] } type GTEl = fields_bw6761.E6 @@ -52,13 +53,16 @@ func NewPairing(api frontend.API) (*Pairing, error) { if err != nil { return nil, fmt.Errorf("new G2 struct: %w", err) } + // thirdRootOne² + thirdRootOne + 1 = 0 in BW6761Fp + thirdRootOne := ba.NewElement("1968985824090209297278610739700577151397666382303825728450741611566800370218827257750865013421937292370006175842381275743914023380727582819905021229583192207421122272650305267822868639090213645505120388400344940985710520836292650") return &Pairing{ - api: api, - Ext6: fields_bw6761.NewExt6(api), - curveF: ba, - curve: curve, - g1: g1, - g2: g2, + api: api, + Ext6: fields_bw6761.NewExt6(api), + curveF: ba, + curve: curve, + g1: g1, + g2: g2, + thirdRootOne: thirdRootOne, }, nil } @@ -223,10 +227,94 @@ func (pr Pairing) PairingCheck(P []*G1Affine, Q []*G2Affine) error { return nil } +func (pr Pairing) IsEqual(x, y *GTEl) frontend.Variable { + return pr.Ext6.IsEqual(x, y) +} + func (pr Pairing) AssertIsEqual(x, y *GTEl) { pr.Ext6.AssertIsEqual(x, y) } +func (pr Pairing) MuxG2(sel frontend.Variable, inputs ...*G2Affine) *G2Affine { + if len(inputs) == 0 { + return nil + } + if len(inputs) == 1 { + pr.api.AssertIsEqual(sel, 0) + return inputs[0] + } + for i := 1; i < len(inputs); i++ { + if (inputs[0].Lines == nil) != (inputs[i].Lines == nil) { + panic("muxing points with and without precomputed lines") + } + } + var ret G2Affine + Xs := make([]*emulated.Element[BaseField], len(inputs)) + Ys := make([]*emulated.Element[BaseField], len(inputs)) + for i := range inputs { + Xs[i] = &inputs[i].P.X + Ys[i] = &inputs[i].P.Y + } + ret.P.X = *pr.curveF.Mux(sel, Xs...) + ret.P.Y = *pr.curveF.Mux(sel, Ys...) + + if inputs[0].Lines == nil { + return &ret + } + + // switch precomputed lines + ret.Lines = new(lineEvaluations) + for j := range inputs[0].Lines[0] { + lineR0s := make([]*emulated.Element[BaseField], len(inputs)) + lineR1s := make([]*emulated.Element[BaseField], len(inputs)) + for k := 0; k < 2; k++ { + for i := range inputs { + lineR0s[i] = &inputs[i].Lines[k][j].R0 + lineR1s[i] = &inputs[i].Lines[k][j].R1 + } + le := &lineEvaluation{ + R0: *pr.curveF.Mux(sel, lineR0s...), + R1: *pr.curveF.Mux(sel, lineR1s...), + } + ret.Lines[k][j] = le + } + } + + return &ret +} + +func (pr Pairing) MuxGt(sel frontend.Variable, inputs ...*GTEl) *GTEl { + if len(inputs) == 0 { + return nil + } + if len(inputs) == 1 { + pr.api.AssertIsEqual(sel, 0) + return inputs[0] + } + var ret GTEl + A0s := make([]*emulated.Element[BaseField], len(inputs)) + A1s := make([]*emulated.Element[BaseField], len(inputs)) + A2s := make([]*emulated.Element[BaseField], len(inputs)) + A3s := make([]*emulated.Element[BaseField], len(inputs)) + A4s := make([]*emulated.Element[BaseField], len(inputs)) + A5s := make([]*emulated.Element[BaseField], len(inputs)) + for i := range inputs { + A0s[i] = &inputs[i].A0 + A1s[i] = &inputs[i].A1 + A2s[i] = &inputs[i].A2 + A3s[i] = &inputs[i].A3 + A4s[i] = &inputs[i].A4 + A5s[i] = &inputs[i].A5 + } + ret.A0 = *pr.curveF.Mux(sel, A0s...) + ret.A1 = *pr.curveF.Mux(sel, A1s...) + ret.A2 = *pr.curveF.Mux(sel, A2s...) + ret.A3 = *pr.curveF.Mux(sel, A3s...) + ret.A4 = *pr.curveF.Mux(sel, A4s...) + ret.A5 = *pr.curveF.Mux(sel, A5s...) + return &ret +} + func (pr Pairing) AssertIsOnCurve(P *G1Affine) { pr.curve.AssertIsOnCurve(P) } @@ -237,8 +325,8 @@ func (pr Pairing) AssertIsOnTwist(Q *G2Affine) { // if Q=(0,0) we assign b=0 otherwise 4, and continue selector := pr.api.And(pr.curveF.IsZero(&Q.P.X), pr.curveF.IsZero(&Q.P.Y)) - bTwist := emulated.ValueOf[BaseField](4) - b := pr.curveF.Select(selector, pr.curveF.Zero(), &bTwist) + bTwist := pr.curveF.NewElement(4) + b := pr.curveF.Select(selector, pr.curveF.Zero(), bTwist) left := pr.curveF.Mul(&Q.P.Y, &Q.P.Y) right := pr.curveF.Mul(&Q.P.X, &Q.P.X) @@ -311,9 +399,6 @@ var loopCounter2 = [190]int8{ 1, 0, 0, 0, 1, 0, -1, 0, -1, 0, 0, 0, 0, 0, 1, 0, 0, 1, } -// thirdRootOne² + thirdRootOne + 1 = 0 in BW6761Fp -var thirdRootOne = emulated.ValueOf[BaseField]("1968985824090209297278610739700577151397666382303825728450741611566800370218827257750865013421937292370006175842381275743914023380727582819905021229583192207421122272650305267822868639090213645505120388400344940985710520836292650") - // MillerLoop computes the optimal Tate multi-Miller loop // (or twisted ate or Eta revisited) // diff --git a/std/algebra/emulated/sw_bw6761/pairing_test.go b/std/algebra/emulated/sw_bw6761/pairing_test.go index 4c10432c..32d2f30e 100644 --- a/std/algebra/emulated/sw_bw6761/pairing_test.go +++ b/std/algebra/emulated/sw_bw6761/pairing_test.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/rand" "fmt" + "math/big" "testing" "github.com/consensys/gnark-crypto/ecc" @@ -266,6 +267,99 @@ func TestGroupMembershipSolve(t *testing.T) { assert.NoError(err) } +type MuxesCircuits struct { + InG2 []G2Affine + InGt []GTEl + SelG2 frontend.Variable + SelGt frontend.Variable + ExpectedG2 G2Affine + ExpectedGt GTEl +} + +func (c *MuxesCircuits) Define(api frontend.API) error { + g2api, err := NewG2(api) + if err != nil { + return fmt.Errorf("new G2: %w", err) + } + pairing, err := NewPairing(api) + if err != nil { + return fmt.Errorf("new pairing: %w", err) + } + var inG2 []*G2Affine + for i := range c.InG2 { + inG2 = append(inG2, &c.InG2[i]) + } + var inGt []*GTEl + for i := range c.InGt { + inGt = append(inGt, &c.InGt[i]) + } + g2 := pairing.MuxG2(c.SelG2, inG2...) + gt := pairing.MuxGt(c.SelGt, inGt...) + if len(c.InG2) == 0 { + if g2 != nil { + return fmt.Errorf("mux G2: expected nil, got %v", g2) + } + } else { + g2api.AssertIsEqual(g2, &c.ExpectedG2) + } + if len(c.InGt) == 0 { + if gt != nil { + return fmt.Errorf("mux Gt: expected nil, got %v", gt) + } + } else { + pairing.AssertIsEqual(gt, &c.ExpectedGt) + } + return nil +} + +func TestPairingMuxes(t *testing.T) { + assert := test.NewAssert(t) + var err error + for _, nbPairs := range []int{0, 1, 2, 3, 4, 5} { + assert.Run(func(assert *test.Assert) { + g2s := make([]bw6761.G2Affine, nbPairs) + gts := make([]bw6761.GT, nbPairs) + var p bw6761.G1Affine + witG2s := make([]G2Affine, nbPairs) + witGts := make([]GTEl, nbPairs) + for i := range nbPairs { + p, g2s[i] = randomG1G2Affines() + gts[i], err = bw6761.Pair([]bw6761.G1Affine{p}, []bw6761.G2Affine{g2s[i]}) + assert.NoError(err) + witG2s[i] = NewG2Affine(g2s[i]) + witGts[i] = NewGTEl(gts[i]) + } + circuit := MuxesCircuits{InG2: make([]G2Affine, nbPairs), InGt: make([]GTEl, nbPairs)} + var witness MuxesCircuits + if nbPairs > 0 { + selG2, err := rand.Int(rand.Reader, big.NewInt(int64(nbPairs))) + assert.NoError(err) + selGt, err := rand.Int(rand.Reader, big.NewInt(int64(nbPairs))) + assert.NoError(err) + expectedG2 := witG2s[selG2.Int64()] + expectedGt := witGts[selGt.Int64()] + witness = MuxesCircuits{ + InG2: witG2s, + InGt: witGts, + SelG2: selG2, + SelGt: selGt, + ExpectedG2: expectedG2, + ExpectedGt: expectedGt, + } + } else { + witness = MuxesCircuits{ + InG2: witG2s, + InGt: witGts, + SelG2: big.NewInt(0), + SelGt: big.NewInt(0), + } + } + err = test.IsSolved(&circuit, &witness, ecc.BN254.ScalarField()) + assert.NoError(err) + }, fmt.Sprintf("nbPairs=%d", nbPairs)) + } +} + // bench func BenchmarkPairing(b *testing.B) { // e(a,2b) * e(-2a,b) == 1 diff --git a/std/algebra/emulated/sw_bw6761/precomputations.go b/std/algebra/emulated/sw_bw6761/precomputations.go index f91db817..24ff7a49 100644 --- a/std/algebra/emulated/sw_bw6761/precomputations.go +++ b/std/algebra/emulated/sw_bw6761/precomputations.go @@ -32,7 +32,7 @@ func precomputeLines(Q bw6761.G2Affine) lineEvaluations { func (p *Pairing) computeLines(Q *g2AffP) lineEvaluations { var cLines lineEvaluations imQ := &g2AffP{ - X: *p.curveF.Mul(&Q.X, &thirdRootOne), + X: *p.curveF.Mul(&Q.X, p.thirdRootOne), Y: *p.curveF.Neg(&Q.Y), } accQ := &g2AffP{ diff --git a/std/algebra/emulated/sw_emulated/doc_test.go b/std/algebra/emulated/sw_emulated/doc_test.go index 291ffafe..be38f79d 100644 --- a/std/algebra/emulated/sw_emulated/doc_test.go +++ b/std/algebra/emulated/sw_emulated/doc_test.go @@ -22,12 +22,16 @@ func (c *ExampleCurveCircuit[B, S]) Define(api frontend.API) error { if err != nil { panic("initialize new curve") } + scalarField, err := emulated.NewField[S](api) + if err != nil { + panic("initialize new field") + } G := curve.Generator() - scalar4 := emulated.ValueOf[S](4) - g4 := curve.ScalarMul(G, &scalar4) // 4*G - scalar5 := emulated.ValueOf[S](5) - g5 := curve.ScalarMul(G, &scalar5) // 5*G - g9 := curve.AddUnified(g4, g5) // 9*G + scalar4 := scalarField.NewElement(4) + g4 := curve.ScalarMul(G, scalar4) // 4*G + scalar5 := scalarField.NewElement(5) + g5 := curve.ScalarMul(G, scalar5) // 5*G + g9 := curve.AddUnified(g4, g5) // 9*G curve.AssertIsEqual(g9, &c.Res) return nil } diff --git a/std/algebra/emulated/sw_emulated/hints.go b/std/algebra/emulated/sw_emulated/hints.go index f00ba4c8..2fe6d109 100644 --- a/std/algebra/emulated/sw_emulated/hints.go +++ b/std/algebra/emulated/sw_emulated/hints.go @@ -21,7 +21,6 @@ import ( "github.com/consensys/gnark/constraint/solver" limbs "github.com/consensys/gnark/std/internal/limbcomposition" "github.com/consensys/gnark/std/math/emulated" - "github.com/consensys/gnark/std/math/emulated/emparams" ) func init() { @@ -95,81 +94,54 @@ func decomposeScalarG1Signs(mod *big.Int, inputs []*big.Int, outputs []*big.Int) // TODO @yelhousni: generalize for any supported curve as it currently supports only: // BN254, BLS12-381, BW6-761 and Secp256k1, P256, P384 and STARK curve. -func scalarMulHint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { - return emulated.UnwrapHintWithNativeInput(inputs, outputs, func(field *big.Int, inputs, outputs []*big.Int) error { +func scalarMulHint(field *big.Int, inputs []*big.Int, outputs []*big.Int) error { + return emulated.UnwrapHintWithNativeInput(inputs, outputs, func(nonNativeField *big.Int, inputs, outputs []*big.Int) error { if len(outputs) != 2 { return errors.New("expecting two outputs") } - if len(outputs) != 2 { - return errors.New("expecting two outputs") + if len(inputs) < 4 { + return errors.New("expecting at least four inputs") } - if field.Cmp(elliptic.P256().Params().P) == 0 { - var fp emparams.P256Fp - var fr emparams.P256Fr - PXLimbs := inputs[:fp.NbLimbs()] - PYLimbs := inputs[fp.NbLimbs() : 2*fp.NbLimbs()] - SLimbs := inputs[2*fp.NbLimbs():] - Px, Py, S := new(big.Int), new(big.Int), new(big.Int) - if err := limbs.Recompose(PXLimbs, fp.BitsPerLimb(), Px); err != nil { - return err - - } - if err := limbs.Recompose(PYLimbs, fp.BitsPerLimb(), Py); err != nil { - return err - - } - if err := limbs.Recompose(SLimbs, fr.BitsPerLimb(), S); err != nil { - return err - - } + effNbBitsBase := inputs[0].Uint64() + effNbBitsScalar := inputs[1].Uint64() + nbPXLimbs := inputs[2].Uint64() + if len(inputs[3:]) < int(nbPXLimbs) { + return fmt.Errorf("expecting %d limbs for the point", nbPXLimbs) + } + PXLimbs := inputs[3 : 3+int(nbPXLimbs)] + nbPYLimbs := inputs[3+int(nbPXLimbs)].Uint64() + if len(inputs[4+int(nbPXLimbs):]) < int(nbPYLimbs) { + return fmt.Errorf("expecting %d limbs for the point", nbPYLimbs) + } + PYLimbs := inputs[4+int(nbPXLimbs) : 4+int(nbPXLimbs)+int(nbPYLimbs)] + nbSLimbs := inputs[4+int(nbPXLimbs)+int(nbPYLimbs)].Uint64() + if len(inputs[5+int(nbPXLimbs)+int(nbPYLimbs):]) != int(nbSLimbs) { + return fmt.Errorf("expecting %d limbs for the scalar", nbSLimbs) + } + SLimbs := inputs[5+int(nbPXLimbs)+int(nbPYLimbs):] + Px, Py, S := new(big.Int), new(big.Int), new(big.Int) + if err := limbs.Recompose(PXLimbs, uint(effNbBitsBase), Px); err != nil { + return fmt.Errorf("failed to recompose Px: %w", err) + } + if err := limbs.Recompose(PYLimbs, uint(effNbBitsBase), Py); err != nil { + return fmt.Errorf("failed to recompose Py: %w", err) + } + if err := limbs.Recompose(SLimbs, uint(effNbBitsScalar), S); err != nil { + return fmt.Errorf("failed to recompose S: %w", err) + } + if nonNativeField.Cmp(elliptic.P256().Params().P) == 0 { curve := elliptic.P256() // compute the resulting point [s]P Qx, Qy := curve.ScalarMult(Px, Py, S.Bytes()) outputs[0].Set(Qx) outputs[1].Set(Qy) - } else if field.Cmp(elliptic.P384().Params().P) == 0 { - var fp emparams.P384Fp - var fr emparams.P384Fr - PXLimbs := inputs[:fp.NbLimbs()] - PYLimbs := inputs[fp.NbLimbs() : 2*fp.NbLimbs()] - SLimbs := inputs[2*fp.NbLimbs():] - Px, Py, S := new(big.Int), new(big.Int), new(big.Int) - if err := limbs.Recompose(PXLimbs, fp.BitsPerLimb(), Px); err != nil { - return err - - } - if err := limbs.Recompose(PYLimbs, fp.BitsPerLimb(), Py); err != nil { - return err - - } - if err := limbs.Recompose(SLimbs, fr.BitsPerLimb(), S); err != nil { - return err - - } + } else if nonNativeField.Cmp(elliptic.P384().Params().P) == 0 { curve := elliptic.P384() // compute the resulting point [s]P Qx, Qy := curve.ScalarMult(Px, Py, S.Bytes()) outputs[0].Set(Qx) outputs[1].Set(Qy) - } else if field.Cmp(stark_fp.Modulus()) == 0 { - var fp emparams.STARKCurveFp - var fr emparams.STARKCurveFr - PXLimbs := inputs[:fp.NbLimbs()] - PYLimbs := inputs[fp.NbLimbs() : 2*fp.NbLimbs()] - SLimbs := inputs[2*fp.NbLimbs():] - Px, Py, S := new(big.Int), new(big.Int), new(big.Int) - if err := limbs.Recompose(PXLimbs, fp.BitsPerLimb(), Px); err != nil { - return err - - } - if err := limbs.Recompose(PYLimbs, fp.BitsPerLimb(), Py); err != nil { - return err - - } - if err := limbs.Recompose(SLimbs, fr.BitsPerLimb(), S); err != nil { - return err - - } + } else if nonNativeField.Cmp(stark_fp.Modulus()) == 0 { // compute the resulting point [s]Q var P stark_curve.G1Affine P.X.SetBigInt(Px) @@ -177,25 +149,7 @@ func scalarMulHint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { P.ScalarMultiplication(&P, S) P.X.BigInt(outputs[0]) P.Y.BigInt(outputs[1]) - } else if field.Cmp(bn_fp.Modulus()) == 0 { - var fp emparams.BN254Fp - var fr emparams.BN254Fr - PXLimbs := inputs[:fp.NbLimbs()] - PYLimbs := inputs[fp.NbLimbs() : 2*fp.NbLimbs()] - SLimbs := inputs[2*fp.NbLimbs():] - Px, Py, S := new(big.Int), new(big.Int), new(big.Int) - if err := limbs.Recompose(PXLimbs, fp.BitsPerLimb(), Px); err != nil { - return err - - } - if err := limbs.Recompose(PYLimbs, fp.BitsPerLimb(), Py); err != nil { - return err - - } - if err := limbs.Recompose(SLimbs, fr.BitsPerLimb(), S); err != nil { - return err - - } + } else if nonNativeField.Cmp(bn_fp.Modulus()) == 0 { // compute the resulting point [s]Q var P bn254.G1Affine P.X.SetBigInt(Px) @@ -203,25 +157,7 @@ func scalarMulHint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { P.ScalarMultiplication(&P, S) P.X.BigInt(outputs[0]) P.Y.BigInt(outputs[1]) - } else if field.Cmp(bls12381_fp.Modulus()) == 0 { - var fp emparams.BLS12381Fp - var fr emparams.BLS12381Fr - PXLimbs := inputs[:fp.NbLimbs()] - PYLimbs := inputs[fp.NbLimbs() : 2*fp.NbLimbs()] - SLimbs := inputs[2*fp.NbLimbs():] - Px, Py, S := new(big.Int), new(big.Int), new(big.Int) - if err := limbs.Recompose(PXLimbs, fp.BitsPerLimb(), Px); err != nil { - return err - - } - if err := limbs.Recompose(PYLimbs, fp.BitsPerLimb(), Py); err != nil { - return err - - } - if err := limbs.Recompose(SLimbs, fr.BitsPerLimb(), S); err != nil { - return err - - } + } else if nonNativeField.Cmp(bls12381_fp.Modulus()) == 0 { // compute the resulting point [s]Q var P bls12381.G1Affine P.X.SetBigInt(Px) @@ -229,25 +165,7 @@ func scalarMulHint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { P.ScalarMultiplication(&P, S) P.X.BigInt(outputs[0]) P.Y.BigInt(outputs[1]) - } else if field.Cmp(secp_fp.Modulus()) == 0 { - var fp emparams.Secp256k1Fp - var fr emparams.Secp256k1Fr - PXLimbs := inputs[:fp.NbLimbs()] - PYLimbs := inputs[fp.NbLimbs() : 2*fp.NbLimbs()] - SLimbs := inputs[2*fp.NbLimbs():] - Px, Py, S := new(big.Int), new(big.Int), new(big.Int) - if err := limbs.Recompose(PXLimbs, fp.BitsPerLimb(), Px); err != nil { - return err - - } - if err := limbs.Recompose(PYLimbs, fp.BitsPerLimb(), Py); err != nil { - return err - - } - if err := limbs.Recompose(SLimbs, fr.BitsPerLimb(), S); err != nil { - return err - - } + } else if nonNativeField.Cmp(secp_fp.Modulus()) == 0 { // compute the resulting point [s]Q var P secp256k1.G1Affine P.X.SetBigInt(Px) @@ -255,25 +173,7 @@ func scalarMulHint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { P.ScalarMultiplication(&P, S) P.X.BigInt(outputs[0]) P.Y.BigInt(outputs[1]) - } else if field.Cmp(bw6_fp.Modulus()) == 0 { - var fp emparams.BW6761Fp - var fr emparams.BW6761Fr - PXLimbs := inputs[:fp.NbLimbs()] - PYLimbs := inputs[fp.NbLimbs() : 2*fp.NbLimbs()] - SLimbs := inputs[2*fp.NbLimbs():] - Px, Py, S := new(big.Int), new(big.Int), new(big.Int) - if err := limbs.Recompose(PXLimbs, fp.BitsPerLimb(), Px); err != nil { - return err - - } - if err := limbs.Recompose(PYLimbs, fp.BitsPerLimb(), Py); err != nil { - return err - - } - if err := limbs.Recompose(SLimbs, fr.BitsPerLimb(), S); err != nil { - return err - - } + } else if nonNativeField.Cmp(bw6_fp.Modulus()) == 0 { // compute the resulting point [s]Q var P bw6761.G1Affine P.X.SetBigInt(Px) @@ -281,7 +181,6 @@ func scalarMulHint(_ *big.Int, inputs []*big.Int, outputs []*big.Int) error { P.ScalarMultiplication(&P, S) P.X.BigInt(outputs[0]) P.Y.BigInt(outputs[1]) - } else { return errors.New("unsupported curve") } diff --git a/std/algebra/emulated/sw_emulated/point.go b/std/algebra/emulated/sw_emulated/point.go index aef54a39..e608e8b8 100644 --- a/std/algebra/emulated/sw_emulated/point.go +++ b/std/algebra/emulated/sw_emulated/point.go @@ -26,10 +26,10 @@ func New[Base, Scalars emulated.FieldParams](api frontend.API, params CurveParam } emuGm := make([]AffinePoint[Base], len(params.Gm)) for i, v := range params.Gm { - emuGm[i] = AffinePoint[Base]{emulated.ValueOf[Base](v[0]), emulated.ValueOf[Base](v[1])} + emuGm[i] = AffinePoint[Base]{*ba.NewElement(v[0]), *ba.NewElement(v[1])} } - Gx := emulated.ValueOf[Base](params.Gx) - Gy := emulated.ValueOf[Base](params.Gy) + Gx := ba.NewElement(params.Gx) + Gy := ba.NewElement(params.Gy) var eigenvalue *emulated.Element[Scalars] var thirdRootOne *emulated.Element[Base] if params.Eigenvalue != nil && params.ThirdRootOne != nil { @@ -42,12 +42,12 @@ func New[Base, Scalars emulated.FieldParams](api frontend.API, params CurveParam baseApi: ba, scalarApi: sa, g: AffinePoint[Base]{ - X: Gx, - Y: Gy, + X: *Gx, + Y: *Gy, }, gm: emuGm, - a: emulated.ValueOf[Base](params.A), - b: emulated.ValueOf[Base](params.B), + a: *ba.NewElement(params.A), + b: *ba.NewElement(params.B), addA: params.A.Cmp(big.NewInt(0)) != 0, eigenvalue: eigenvalue, thirdRootOne: thirdRootOne, @@ -492,6 +492,10 @@ func (c *Curve[B, S]) Mux(sel frontend.Variable, inputs ...*AffinePoint[B]) *Aff // This function doesn't check that the p is on the curve. See AssertIsOnCurve. // // ScalarMul calls scalarMulFakeGLV or scalarMulGLVAndFakeGLV depending on whether an efficient endomorphism is available. +// +// N.B. For scalarMulGLVAndFakeGLV, the result is undefined when the input point is +// not on the prime order subgroup. For scalarMulFakeGLV the result is well +// defined for any point on the curve func (c *Curve[B, S]) ScalarMul(p *AffinePoint[B], s *emulated.Element[S], opts ...algopts.AlgebraOption) *AffinePoint[B] { if c.eigenvalue != nil && c.thirdRootOne != nil { return c.scalarMulGLVAndFakeGLV(p, s, opts...) @@ -726,6 +730,9 @@ func (c *Curve[B, S]) scalarMulGLV(Q *AffinePoint[B], s *emulated.Element[S], op // positions 1 and n-1 outside of the loop to optimize the number of // constraints using [ELM03] (Section 3.1) // +// Contrary to the GLV method, this method doesn't require the endomorphism and +// thus is also suitable for points not in the prime order subgroup. +// // [ELM03]: https://arxiv.org/pdf/math/0208038.pdf // [EVM]: https://ethereum.github.io/yellowpaper/paper.pdf // [Joye07]: https://www.iacr.org/archive/ches2007/47270135/47270135.pdf @@ -851,8 +858,8 @@ func (c *Curve[B, S]) jointScalarMulGLV(p1, p2 *AffinePoint[B], s1, s2 *emulated panic(fmt.Sprintf("parse opts: %v", err)) } if cfg.CompleteArithmetic { - res1 := c.scalarMulGLV(p1, s1, opts...) - res2 := c.scalarMulGLV(p2, s2, opts...) + res1 := c.scalarMulGLVAndFakeGLV(p1, s1, opts...) + res2 := c.scalarMulGLVAndFakeGLV(p2, s2, opts...) return c.AddUnified(res1, res2) } else { return c.jointScalarMulGLVUnsafe(p1, p2, s1, s2) @@ -1094,10 +1101,10 @@ func (c *Curve[B, S]) jointScalarMulGLVUnsafe(Q, R *AffinePoint[B], s, t *emulat // ScalarMulBase computes [s]g and returns it where g is the fixed curve generator. It doesn't modify p nor s. // -// ScalarMul calls scalarMulBaseGeneric or scalarMulGLV depending on whether an efficient endomorphism is available. +// ScalarMul calls scalarMulBaseGeneric or scalarMulGLVAndFakeGLV depending on whether an efficient endomorphism is available. func (c *Curve[B, S]) ScalarMulBase(s *emulated.Element[S], opts ...algopts.AlgebraOption) *AffinePoint[B] { if c.eigenvalue != nil && c.thirdRootOne != nil { - return c.scalarMulGLV(c.Generator(), s, opts...) + return c.scalarMulGLVAndFakeGLV(c.Generator(), s, opts...) } else { return c.scalarMulBaseGeneric(s, opts...) @@ -1234,7 +1241,7 @@ func (c *Curve[B, S]) MultiScalarMul(p []*AffinePoint[B], s []*emulated.Element[ } // scalarMulFakeGLV computes [s]Q and returns it. It doesn't modify Q nor s. -// It implements the "fake GLV" explained in: https://hackmd.io/@yelhousni/Hy-aWld50. +// It implements the "fake GLV" explained in [EEMP25] (Sec. 3.1). // // ⚠️ The scalar s must be nonzero and the point Q different from (0,0) unless [algopts.WithCompleteArithmetic] is set. // (0,0) is not on the curve but we conventionally take it as the @@ -1244,6 +1251,7 @@ func (c *Curve[B, S]) MultiScalarMul(p []*AffinePoint[B], s []*emulated.Element[ // P256, P384 and STARK curve. // // [EVM]: https://ethereum.github.io/yellowpaper/paper.pdf +// [EEMP25]: https://eprint.iacr.org/2025/933 func (c *Curve[B, S]) scalarMulFakeGLV(Q *AffinePoint[B], s *emulated.Element[S], opts ...algopts.AlgebraOption) *AffinePoint[B] { cfg, err := algopts.NewConfig(opts...) if err != nil { @@ -1282,9 +1290,26 @@ func (c *Curve[B, S]) scalarMulFakeGLV(Q *AffinePoint[B], s *emulated.Element[S] // Then we compute the hinted scalar mul R = [s]Q // Q coordinates are in Fp and the scalar s in Fr // we decompose Q.X, Q.Y, s into limbs and recompose them in the hint. + + // but first - in some edge cases it is possible that we compute the scalar multiplication + // for a constant scalar and constant point. This happens when the recursive SNARK verifier + // is used with a static verification key for example. Usually, the non-native element is always + // lazily initialized during witness parsing, circuit compilation and non-native arithmetic time. + // However here none of the cases applies and we perform operation directly on limbs of non-native element. + // So we initialize it here. + Q.X.Initialize(c.api.Compiler().Field()) + Q.Y.Initialize(c.api.Compiler().Field()) + s.Initialize(c.api.Compiler().Field()) + var inps []frontend.Variable + _, effNbBitsB := emulated.GetEffectiveFieldParams[B](c.api.Compiler().Field()) + _, effNbBitsS := emulated.GetEffectiveFieldParams[S](c.api.Compiler().Field()) + inps = append(inps, effNbBitsB, effNbBitsS) + inps = append(inps, len(Q.X.Limbs)) inps = append(inps, Q.X.Limbs...) + inps = append(inps, len(Q.Y.Limbs)) inps = append(inps, Q.Y.Limbs...) + inps = append(inps, len(s.Limbs)) inps = append(inps, s.Limbs...) R, err := c.baseApi.NewHintWithNativeInput(scalarMulHint, 2, inps...) if err != nil { @@ -1512,17 +1537,19 @@ func (c *Curve[B, S]) scalarMulFakeGLV(Q *AffinePoint[B], s *emulated.Element[S] } // scalarMulGLVAndFakeGLV computes [s]P and returns it. It doesn't modify P nor s. -// It implements the "GLV + fake GLV" explained in [ethresear.ch/fake-GLV]. +// It implements the "GLV + fake GLV" explained in [EEMP25] (Sec. 3.3). // // ⚠️ The scalar s must be nonzero and the point Q different from (0,0) unless [algopts.WithCompleteArithmetic] is set. // (0,0) is not on the curve but we conventionally take it as the // neutral/infinity point as per the [EVM]. // +// The result is undefined for input points that are not in the prime subgroup. +// // TODO @yelhousni: generalize for any supported curve as it currently supports only: // BN254, BLS12-381, BW6-761 and Secp256k1. // -// [ethresear.ch/fake-GLV]: https://ethresear.ch/t/fake-glv-you-dont-need-an-efficient-endomorphism-to-implement-glv-like-scalar-multiplication-in-snark-circuits/20394 // [EVM]: https://ethereum.github.io/yellowpaper/paper.pdf +// [EEMP25]: https://eprint.iacr.org/2025/933 func (c *Curve[B, S]) scalarMulGLVAndFakeGLV(P *AffinePoint[B], s *emulated.Element[S], opts ...algopts.AlgebraOption) *AffinePoint[B] { cfg, err := algopts.NewConfig(opts...) if err != nil { @@ -1612,9 +1639,26 @@ func (c *Curve[B, S]) scalarMulGLVAndFakeGLV(P *AffinePoint[B], s *emulated.Elem // Next we compute the hinted scalar mul Q = [s]P // P coordinates are in Fp and the scalar s in Fr // we decompose Q.X, Q.Y, s into limbs and recompose them in the hint. + + // but first - in some edge cases it is possible that we compute the scalar multiplication + // for a constant scalar and constant point. This happens when the recursive SNARK verifier + // is used with a static verification key for example. Usually, the non-native element is always + // lazily initialized during witness parsing, circuit compilation and non-native arithmetic time. + // However here none of the cases applies and we perform operation directly on limbs of non-native element. + // So we initialize it here. + P.X.Initialize(c.api.Compiler().Field()) + P.Y.Initialize(c.api.Compiler().Field()) + s.Initialize(c.api.Compiler().Field()) + var inps []frontend.Variable + _, effNbBitsB := emulated.GetEffectiveFieldParams[B](c.api.Compiler().Field()) + _, effNbBitsS := emulated.GetEffectiveFieldParams[S](c.api.Compiler().Field()) + inps = append(inps, effNbBitsB, effNbBitsS) + inps = append(inps, len(P.X.Limbs)) inps = append(inps, P.X.Limbs...) + inps = append(inps, len(P.Y.Limbs)) inps = append(inps, P.Y.Limbs...) + inps = append(inps, len(s.Limbs)) inps = append(inps, s.Limbs...) point, err := c.baseApi.NewHintWithNativeInput(scalarMulHint, 2, inps...) if err != nil { diff --git a/std/algebra/emulated/sw_emulated/point_test.go b/std/algebra/emulated/sw_emulated/point_test.go index 7e96cc3a..6b127e18 100644 --- a/std/algebra/emulated/sw_emulated/point_test.go +++ b/std/algebra/emulated/sw_emulated/point_test.go @@ -2437,6 +2437,25 @@ func TestScalarMulGLVAndFakeGLVEdgeCasesEdgeCases(t *testing.T) { } err = test.IsSolved(&circuit, &witness5, testCurve.ScalarField()) assert.NoError(err) + + // -2 * P == -2P + minusTwo := big.NewInt(-2) + var expected secp256k1.G1Affine + expected.ScalarMultiplication(&g, minusTwo) + witness6 := ScalarMulGLVAndFakeGLVEdgeCasesTest[emulated.Secp256k1Fp, emulated.Secp256k1Fr]{ + S: emulated.ValueOf[emulated.Secp256k1Fr](minusTwo), + P: AffinePoint[emulated.Secp256k1Fp]{ + X: emulated.ValueOf[emulated.Secp256k1Fp](g.X), + Y: emulated.ValueOf[emulated.Secp256k1Fp](g.Y), + }, + R: AffinePoint[emulated.Secp256k1Fp]{ + X: emulated.ValueOf[emulated.Secp256k1Fp](expected.X), + Y: emulated.ValueOf[emulated.Secp256k1Fp](expected.Y), + }, + } + + err = test.IsSolved(&circuit, &witness6, testCurve.ScalarField()) + assert.NoError(err) } func TestScalarMulGLVAndFakeGLVEdgeCasesEdgeCases2(t *testing.T) { diff --git a/std/algebra/interfaces.go b/std/algebra/interfaces.go index 3775d0f9..555cb03f 100644 --- a/std/algebra/interfaces.go +++ b/std/algebra/interfaces.go @@ -105,4 +105,17 @@ type Pairing[G1El G1ElementT, G2El G2ElementT, GtEl GtElementT] interface { // AssertIsOnG2 asserts that the input is on the G2 curve. AssertIsOnG2(*G2El) + + // 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. + MuxG2(sel frontend.Variable, inputs ...*G2El) *G2El + + // MuxGt performs a lookup from the Gt inputs and returns inputs[sel]. It is + // most efficient for power of two lengths of the inputs, but works for any + // number of inputs. + MuxGt(sel frontend.Variable, inputs ...*GtEl) *GtEl + + // IsEqual checks if the two inputs are equal. It returns a frontend.Variable. + IsEqual(a, b *GtEl) frontend.Variable } diff --git a/std/algebra/native/sw_bls12377/pairing2.go b/std/algebra/native/sw_bls12377/pairing2.go index 5a063603..50f2366d 100644 --- a/std/algebra/native/sw_bls12377/pairing2.go +++ b/std/algebra/native/sw_bls12377/pairing2.go @@ -106,6 +106,42 @@ func (c *Curve) AssertIsEqual(P, Q *G1Affine) { P.AssertIsEqual(c.api, *Q) } +func (c *Pairing) IsEqual(x, y *GT) frontend.Variable { + diff0 := c.api.Sub(&x.C0.B0.A0, &y.C0.B0.A0) + diff1 := c.api.Sub(&x.C0.B0.A1, &y.C0.B0.A1) + diff2 := c.api.Sub(&x.C0.B0.A0, &y.C0.B0.A0) + diff3 := c.api.Sub(&x.C0.B1.A1, &y.C0.B1.A1) + diff4 := c.api.Sub(&x.C0.B1.A0, &y.C0.B1.A0) + diff5 := c.api.Sub(&x.C0.B1.A1, &y.C0.B1.A1) + diff6 := c.api.Sub(&x.C1.B0.A0, &y.C1.B0.A0) + diff7 := c.api.Sub(&x.C1.B0.A1, &y.C1.B0.A1) + diff8 := c.api.Sub(&x.C1.B0.A0, &y.C1.B0.A0) + diff9 := c.api.Sub(&x.C1.B1.A1, &y.C1.B1.A1) + diff10 := c.api.Sub(&x.C1.B1.A0, &y.C1.B1.A0) + diff11 := c.api.Sub(&x.C1.B1.A1, &y.C1.B1.A1) + + isZero0 := c.api.IsZero(diff0) + isZero1 := c.api.IsZero(diff1) + isZero2 := c.api.IsZero(diff2) + isZero3 := c.api.IsZero(diff3) + isZero4 := c.api.IsZero(diff4) + isZero5 := c.api.IsZero(diff5) + isZero6 := c.api.IsZero(diff6) + isZero7 := c.api.IsZero(diff7) + isZero8 := c.api.IsZero(diff8) + isZero9 := c.api.IsZero(diff9) + isZero10 := c.api.IsZero(diff10) + isZero11 := c.api.IsZero(diff11) + + return c.api.And( + c.api.And( + c.api.And(c.api.And(isZero0, isZero1), c.api.And(isZero2, isZero3)), + c.api.And(c.api.And(isZero4, isZero5), c.api.And(isZero6, isZero7)), + ), + c.api.And(c.api.And(isZero8, isZero9), c.api.And(isZero10, isZero11)), + ) +} + // Neg negates P and returns the result. Does not modify P. func (c *Curve) Neg(P *G1Affine) *G1Affine { res := &G1Affine{ @@ -326,6 +362,120 @@ func (p *Pairing) AssertIsEqual(e1, e2 *GT) { e1.AssertIsEqual(p.api, *e2) } +func (pr Pairing) MuxG2(sel frontend.Variable, inputs ...*G2Affine) *G2Affine { + if len(inputs) == 0 { + return nil + } + if len(inputs) == 1 { + pr.api.AssertIsEqual(sel, 0) + return inputs[0] + } + for i := 1; i < len(inputs); i++ { + if (inputs[0].Lines == nil) != (inputs[i].Lines == nil) { + panic("muxing points with and without precomputed lines") + } + } + var ret G2Affine + XA0 := make([]frontend.Variable, len(inputs)) + XA1 := make([]frontend.Variable, len(inputs)) + YA0 := make([]frontend.Variable, len(inputs)) + YA1 := make([]frontend.Variable, len(inputs)) + for i := range inputs { + XA0[i] = inputs[i].P.X.A0 + XA1[i] = inputs[i].P.X.A1 + YA0[i] = inputs[i].P.Y.A0 + YA1[i] = inputs[i].P.Y.A1 + } + ret.P.X.A0 = selector.Mux(pr.api, sel, XA0...) + ret.P.X.A1 = selector.Mux(pr.api, sel, XA1...) + ret.P.Y.A0 = selector.Mux(pr.api, sel, YA0...) + ret.P.Y.A1 = selector.Mux(pr.api, sel, YA1...) + + if inputs[0].Lines == nil { + return &ret + } + + // switch precomputed lines + ret.Lines = new(lineEvaluations) + for j := range inputs[0].Lines[0] { + lineR0A0 := make([]frontend.Variable, len(inputs)) + lineR0A1 := make([]frontend.Variable, len(inputs)) + lineR1A0 := make([]frontend.Variable, len(inputs)) + lineR1A1 := make([]frontend.Variable, len(inputs)) + for k := 0; k < 2; k++ { + for i := range inputs { + lineR0A0[i] = inputs[i].Lines[k][j].R0.A0 + lineR0A1[i] = inputs[i].Lines[k][j].R0.A1 + lineR1A0[i] = inputs[i].Lines[k][j].R1.A0 + lineR1A1[i] = inputs[i].Lines[k][j].R1.A1 + } + le := &lineEvaluation{ + R0: fields_bls12377.E2{ + A0: selector.Mux(pr.api, sel, lineR0A0...), + A1: selector.Mux(pr.api, sel, lineR0A1...), + }, + R1: fields_bls12377.E2{ + A0: selector.Mux(pr.api, sel, lineR1A0...), + A1: selector.Mux(pr.api, sel, lineR1A1...), + }, + } + ret.Lines[k][j] = le + } + } + + return &ret +} + +func (pr Pairing) MuxGt(sel frontend.Variable, inputs ...*GT) *GT { + if len(inputs) == 0 { + return nil + } + if len(inputs) == 1 { + pr.api.AssertIsEqual(sel, 0) + return inputs[0] + } + var ret GT + C0B0A0s := make([]frontend.Variable, len(inputs)) + C0B0A1s := make([]frontend.Variable, len(inputs)) + C0B1A0s := make([]frontend.Variable, len(inputs)) + C0B1A1s := make([]frontend.Variable, len(inputs)) + C0B2A0s := make([]frontend.Variable, len(inputs)) + C0B2A1s := make([]frontend.Variable, len(inputs)) + C1B0A0s := make([]frontend.Variable, len(inputs)) + C1B0A1s := make([]frontend.Variable, len(inputs)) + C1B1A0s := make([]frontend.Variable, len(inputs)) + C1B1A1s := make([]frontend.Variable, len(inputs)) + C1B2A0s := make([]frontend.Variable, len(inputs)) + C1B2A1s := make([]frontend.Variable, len(inputs)) + for i := range inputs { + C0B0A0s[i] = inputs[i].C0.B0.A0 + C0B0A1s[i] = inputs[i].C0.B0.A1 + C0B1A0s[i] = inputs[i].C0.B1.A0 + C0B1A1s[i] = inputs[i].C0.B1.A1 + C0B2A0s[i] = inputs[i].C0.B2.A0 + C0B2A1s[i] = inputs[i].C0.B2.A1 + C1B0A0s[i] = inputs[i].C1.B0.A0 + C1B0A1s[i] = inputs[i].C1.B0.A1 + C1B1A0s[i] = inputs[i].C1.B1.A0 + C1B1A1s[i] = inputs[i].C1.B1.A1 + C1B2A0s[i] = inputs[i].C1.B2.A0 + C1B2A1s[i] = inputs[i].C1.B2.A1 + } + ret.C0.B0.A0 = selector.Mux(pr.api, sel, C0B0A0s...) + ret.C0.B0.A1 = selector.Mux(pr.api, sel, C0B0A1s...) + ret.C0.B1.A0 = selector.Mux(pr.api, sel, C0B1A0s...) + ret.C0.B1.A1 = selector.Mux(pr.api, sel, C0B1A1s...) + ret.C0.B2.A0 = selector.Mux(pr.api, sel, C0B2A0s...) + ret.C0.B2.A1 = selector.Mux(pr.api, sel, C0B2A1s...) + ret.C1.B0.A0 = selector.Mux(pr.api, sel, C1B0A0s...) + ret.C1.B0.A1 = selector.Mux(pr.api, sel, C1B0A1s...) + ret.C1.B1.A0 = selector.Mux(pr.api, sel, C1B1A0s...) + ret.C1.B1.A1 = selector.Mux(pr.api, sel, C1B1A1s...) + ret.C1.B2.A0 = selector.Mux(pr.api, sel, C1B2A0s...) + ret.C1.B2.A1 = selector.Mux(pr.api, sel, C1B2A1s...) + return &ret +} + // AssertIsOnCurve asserts if p belongs to the curve. It doesn't modify p. func (c *Pairing) AssertIsOnCurve(p *G1Affine) { // (X,Y) ∈ {Y² == X³ + 1} U (0,0) diff --git a/std/algebra/native/sw_bls12377/pairing2_test.go b/std/algebra/native/sw_bls12377/pairing2_test.go index cd1bf9e8..450a2273 100644 --- a/std/algebra/native/sw_bls12377/pairing2_test.go +++ b/std/algebra/native/sw_bls12377/pairing2_test.go @@ -2,6 +2,7 @@ package sw_bls12377 import ( "crypto/rand" + "fmt" "math/big" "testing" @@ -12,13 +13,31 @@ import ( "github.com/consensys/gnark/test" ) -type MuxCircuitTest struct { +func randomG1G2Affines() (bls12377.G1Affine, bls12377.G2Affine) { + _, _, G1AffGen, G2AffGen := bls12377.Generators() + mod := bls12377.ID.ScalarField() + s1, err := rand.Int(rand.Reader, mod) + if err != nil { + panic(err) + } + s2, err := rand.Int(rand.Reader, mod) + if err != nil { + panic(err) + } + var p bls12377.G1Affine + p.ScalarMultiplication(&G1AffGen, s1) + var q bls12377.G2Affine + q.ScalarMultiplication(&G2AffGen, s2) + return p, q +} + +type MuxG1CircuitTest struct { Selector frontend.Variable Inputs [8]G1Affine Expected G1Affine } -func (c *MuxCircuitTest) Define(api frontend.API) error { +func (c *MuxG1CircuitTest) Define(api frontend.API) error { cr, err := NewCurve(api) if err != nil { return err @@ -32,9 +51,9 @@ func (c *MuxCircuitTest) Define(api frontend.API) error { return nil } -func TestMux(t *testing.T) { +func TestMuxG1(t *testing.T) { assert := test.NewAssert(t) - circuit := MuxCircuitTest{} + circuit := MuxG1CircuitTest{} r := make([]fr_bls12377.Element, len(circuit.Inputs)) for i := range r { r[i].SetRandom() @@ -42,7 +61,7 @@ func TestMux(t *testing.T) { selector, _ := rand.Int(rand.Reader, big.NewInt(int64(len(r)))) expectedR := r[selector.Int64()] expected := new(bls12377.G1Affine).ScalarMultiplicationBase(expectedR.BigInt(new(big.Int))) - witness := MuxCircuitTest{ + witness := MuxG1CircuitTest{ Selector: selector, Expected: NewG1Affine(*expected), } @@ -53,3 +72,91 @@ func TestMux(t *testing.T) { err := test.IsSolved(&circuit, &witness, ecc.BW6_761.ScalarField()) assert.NoError(err) } + +type MuxG2GtCircuit struct { + InG2 []G2Affine + InGt []GT + SelG2 frontend.Variable + SelGt frontend.Variable + ExpectedG2 G2Affine + ExpectedGt GT +} + +func (c *MuxG2GtCircuit) Define(api frontend.API) error { + pairing := NewPairing(api) + var inG2 []*G2Affine + for i := range c.InG2 { + inG2 = append(inG2, &c.InG2[i]) + } + var inGt []*GT + for i := range c.InGt { + inGt = append(inGt, &c.InGt[i]) + } + g2 := pairing.MuxG2(c.SelG2, inG2...) + gt := pairing.MuxGt(c.SelGt, inGt...) + if len(c.InG2) == 0 { + if g2 != nil { + return fmt.Errorf("mux G2: expected nil, got %v", g2) + } + } else { + c.ExpectedG2.P.AssertIsEqual(api, g2.P) + } + if len(c.InGt) == 0 { + if gt != nil { + return fmt.Errorf("mux Gt: expected nil, got %v", gt) + } + } else { + pairing.AssertIsEqual(gt, &c.ExpectedGt) + } + return nil +} + +func TestPairingMuxes(t *testing.T) { + assert := test.NewAssert(t) + var err error + for _, nbPairs := range []int{0, 1, 2, 3, 4, 5} { + assert.Run(func(assert *test.Assert) { + g2s := make([]bls12377.G2Affine, nbPairs) + gts := make([]bls12377.GT, nbPairs) + var p bls12377.G1Affine + witG2s := make([]G2Affine, nbPairs) + witGts := make([]GT, nbPairs) + for i := range nbPairs { + p, g2s[i] = randomG1G2Affines() + gts[i], err = bls12377.Pair([]bls12377.G1Affine{p}, []bls12377.G2Affine{g2s[i]}) + assert.NoError(err) + witG2s[i] = NewG2Affine(g2s[i]) + witGts[i] = NewGTEl(gts[i]) + } + circuit := MuxG2GtCircuit{InG2: make([]G2Affine, nbPairs), InGt: make([]GT, nbPairs)} + var witness MuxG2GtCircuit + if nbPairs > 0 { + selG2, err := rand.Int(rand.Reader, big.NewInt(int64(nbPairs))) + assert.NoError(err) + selGt, err := rand.Int(rand.Reader, big.NewInt(int64(nbPairs))) + assert.NoError(err) + expectedG2 := witG2s[selG2.Int64()] + expectedGt := witGts[selGt.Int64()] + witness = MuxG2GtCircuit{ + InG2: witG2s, + InGt: witGts, + SelG2: selG2, + SelGt: selGt, + ExpectedG2: expectedG2, + ExpectedGt: expectedGt, + } + } else { + witness = MuxG2GtCircuit{ + InG2: witG2s, + InGt: witGts, + SelG2: big.NewInt(0), + SelGt: big.NewInt(0), + ExpectedG2: NewG2Affine(bls12377.G2Affine{}), + ExpectedGt: NewGTEl(bls12377.GT{}), + } + } + err = test.IsSolved(&circuit, &witness, ecc.BW6_761.ScalarField()) + assert.NoError(err) + }, fmt.Sprintf("nbPairs=%d", nbPairs)) + } +} diff --git a/std/algebra/native/sw_bls24315/pairing2.go b/std/algebra/native/sw_bls24315/pairing2.go index c213b4b5..bdc3d21f 100644 --- a/std/algebra/native/sw_bls24315/pairing2.go +++ b/std/algebra/native/sw_bls24315/pairing2.go @@ -255,6 +255,75 @@ func NewPairing(api frontend.API) *Pairing { } } +func (c *Pairing) IsEqual(x, y *GT) frontend.Variable { + diff0 := c.api.Sub(&x.D0.C0.B0.A0, &y.D0.C0.B0.A0) + diff1 := c.api.Sub(&x.D0.C0.B0.A1, &y.D0.C0.B0.A1) + diff2 := c.api.Sub(&x.D0.C0.B0.A0, &y.D0.C0.B0.A0) + diff3 := c.api.Sub(&x.D0.C0.B1.A1, &y.D0.C0.B1.A1) + diff4 := c.api.Sub(&x.D0.C0.B1.A0, &y.D0.C0.B1.A0) + diff5 := c.api.Sub(&x.D0.C0.B1.A1, &y.D0.C0.B1.A1) + diff6 := c.api.Sub(&x.D0.C1.B0.A0, &y.D0.C1.B0.A0) + diff7 := c.api.Sub(&x.D0.C1.B0.A1, &y.D0.C1.B0.A1) + diff8 := c.api.Sub(&x.D0.C1.B0.A0, &y.D0.C1.B0.A0) + diff9 := c.api.Sub(&x.D0.C1.B1.A1, &y.D0.C1.B1.A1) + diff10 := c.api.Sub(&x.D0.C1.B1.A0, &y.D0.C1.B1.A0) + diff11 := c.api.Sub(&x.D0.C1.B1.A1, &y.D0.C1.B1.A1) + diff12 := c.api.Sub(&x.D1.C0.B0.A0, &y.D1.C0.B0.A0) + diff13 := c.api.Sub(&x.D1.C0.B0.A1, &y.D1.C0.B0.A1) + diff14 := c.api.Sub(&x.D1.C0.B0.A0, &y.D1.C0.B0.A0) + diff15 := c.api.Sub(&x.D1.C0.B1.A1, &y.D1.C0.B1.A1) + diff16 := c.api.Sub(&x.D1.C0.B1.A0, &y.D1.C0.B1.A0) + diff17 := c.api.Sub(&x.D1.C0.B1.A1, &y.D1.C0.B1.A1) + diff18 := c.api.Sub(&x.D1.C1.B0.A0, &y.D1.C1.B0.A0) + diff19 := c.api.Sub(&x.D1.C1.B0.A1, &y.D1.C1.B0.A1) + diff20 := c.api.Sub(&x.D1.C1.B0.A0, &y.D1.C1.B0.A0) + diff21 := c.api.Sub(&x.D1.C1.B1.A1, &y.D1.C1.B1.A1) + diff22 := c.api.Sub(&x.D1.C1.B1.A0, &y.D1.C1.B1.A0) + diff23 := c.api.Sub(&x.D1.C1.B1.A1, &y.D1.C1.B1.A1) + + isZero0 := c.api.IsZero(diff0) + isZero1 := c.api.IsZero(diff1) + isZero2 := c.api.IsZero(diff2) + isZero3 := c.api.IsZero(diff3) + isZero4 := c.api.IsZero(diff4) + isZero5 := c.api.IsZero(diff5) + isZero6 := c.api.IsZero(diff6) + isZero7 := c.api.IsZero(diff7) + isZero8 := c.api.IsZero(diff8) + isZero9 := c.api.IsZero(diff9) + isZero10 := c.api.IsZero(diff10) + isZero11 := c.api.IsZero(diff11) + isZero12 := c.api.IsZero(diff12) + isZero13 := c.api.IsZero(diff13) + isZero14 := c.api.IsZero(diff14) + isZero15 := c.api.IsZero(diff15) + isZero16 := c.api.IsZero(diff16) + isZero17 := c.api.IsZero(diff17) + isZero18 := c.api.IsZero(diff18) + isZero19 := c.api.IsZero(diff19) + isZero20 := c.api.IsZero(diff20) + isZero21 := c.api.IsZero(diff21) + isZero22 := c.api.IsZero(diff22) + isZero23 := c.api.IsZero(diff23) + + return c.api.And( + c.api.And( + c.api.And( + c.api.And(c.api.And(isZero0, isZero1), c.api.And(isZero2, isZero3)), + c.api.And(c.api.And(isZero4, isZero5), c.api.And(isZero6, isZero7)), + ), + c.api.And( + c.api.And(c.api.And(isZero8, isZero9), c.api.And(isZero10, isZero11)), + c.api.And(c.api.And(isZero12, isZero13), c.api.And(isZero14, isZero15)), + ), + ), + c.api.And( + c.api.And(c.api.And(isZero16, isZero17), c.api.And(isZero18, isZero19)), + c.api.And(c.api.And(isZero20, isZero21), c.api.And(isZero22, isZero23)), + ), + ) +} + // MillerLoop computes the Miller loop between the pairs of inputs. It doesn't // modify the inputs. It returns an error if there is a mismatch between the // lengths of the inputs. @@ -318,6 +387,188 @@ func (p *Pairing) PairingCheck(P []*G1Affine, Q []*G2Affine) error { func (p *Pairing) AssertIsEqual(e1, e2 *GT) { e1.AssertIsEqual(p.api, *e2) } +func (pr Pairing) MuxG2(sel frontend.Variable, inputs ...*G2Affine) *G2Affine { + if len(inputs) == 0 { + return nil + } + if len(inputs) == 1 { + pr.api.AssertIsEqual(sel, 0) + return inputs[0] + } + for i := 1; i < len(inputs); i++ { + if (inputs[0].Lines == nil) != (inputs[i].Lines == nil) { + panic("muxing points with and without precomputed lines") + } + } + var ret G2Affine + XB0A0 := make([]frontend.Variable, len(inputs)) + XB0A1 := make([]frontend.Variable, len(inputs)) + XB1A0 := make([]frontend.Variable, len(inputs)) + XB1A1 := make([]frontend.Variable, len(inputs)) + YB0A0 := make([]frontend.Variable, len(inputs)) + YB0A1 := make([]frontend.Variable, len(inputs)) + YB1A0 := make([]frontend.Variable, len(inputs)) + YB1A1 := make([]frontend.Variable, len(inputs)) + for i := range inputs { + XB0A0[i] = inputs[i].P.X.B0.A0 + XB0A1[i] = inputs[i].P.X.B0.A1 + XB1A0[i] = inputs[i].P.X.B1.A0 + XB1A1[i] = inputs[i].P.X.B1.A1 + YB0A0[i] = inputs[i].P.Y.B0.A0 + YB0A1[i] = inputs[i].P.Y.B0.A1 + YB1A0[i] = inputs[i].P.Y.B1.A0 + YB1A1[i] = inputs[i].P.Y.B1.A1 + } + ret.P.X.B0.A0 = selector.Mux(pr.api, sel, XB0A0...) + ret.P.X.B0.A1 = selector.Mux(pr.api, sel, XB0A1...) + ret.P.X.B1.A0 = selector.Mux(pr.api, sel, XB1A0...) + ret.P.X.B1.A1 = selector.Mux(pr.api, sel, XB1A1...) + ret.P.Y.B0.A0 = selector.Mux(pr.api, sel, YB0A0...) + ret.P.Y.B0.A1 = selector.Mux(pr.api, sel, YB0A1...) + ret.P.Y.B1.A0 = selector.Mux(pr.api, sel, YB1A0...) + ret.P.Y.B1.A1 = selector.Mux(pr.api, sel, YB1A1...) + + if inputs[0].Lines == nil { + return &ret + } + + // switch precomputed lines + ret.Lines = new(lineEvaluations) + for j := range inputs[0].Lines[0] { + lineR0B0A0 := make([]frontend.Variable, len(inputs)) + lineR0B0A1 := make([]frontend.Variable, len(inputs)) + lineR0B1A0 := make([]frontend.Variable, len(inputs)) + lineR0B1A1 := make([]frontend.Variable, len(inputs)) + lineR1B0A0 := make([]frontend.Variable, len(inputs)) + lineR1B0A1 := make([]frontend.Variable, len(inputs)) + lineR1B1A0 := make([]frontend.Variable, len(inputs)) + lineR1B1A1 := make([]frontend.Variable, len(inputs)) + for k := 0; k < 2; k++ { + for i := range inputs { + lineR0B0A0[i] = inputs[i].Lines[k][j].R0.B0.A0 + lineR0B0A1[i] = inputs[i].Lines[k][j].R0.B0.A1 + lineR0B1A0[i] = inputs[i].Lines[k][j].R0.B1.A0 + lineR0B1A1[i] = inputs[i].Lines[k][j].R0.B1.A1 + lineR1B0A0[i] = inputs[i].Lines[k][j].R1.B0.A0 + lineR1B0A1[i] = inputs[i].Lines[k][j].R1.B0.A1 + lineR1B1A0[i] = inputs[i].Lines[k][j].R1.B1.A0 + lineR1B1A1[i] = inputs[i].Lines[k][j].R1.B1.A1 + } + le := &lineEvaluation{ + R0: fields_bls24315.E4{ + B0: fields_bls24315.E2{ + A0: selector.Mux(pr.api, sel, lineR0B0A0...), + A1: selector.Mux(pr.api, sel, lineR0B0A1...), + }, + B1: fields_bls24315.E2{ + A0: selector.Mux(pr.api, sel, lineR0B1A0...), + A1: selector.Mux(pr.api, sel, lineR0B1A1...), + }, + }, + R1: fields_bls24315.E4{ + B0: fields_bls24315.E2{ + A0: selector.Mux(pr.api, sel, lineR1B0A0...), + A1: selector.Mux(pr.api, sel, lineR1B0A1...), + }, + B1: fields_bls24315.E2{ + A0: selector.Mux(pr.api, sel, lineR1B1A0...), + A1: selector.Mux(pr.api, sel, lineR1B1A1...), + }, + }, + } + ret.Lines[k][j] = le + } + } + + return &ret +} + +func (pr Pairing) MuxGt(sel frontend.Variable, inputs ...*GT) *GT { + if len(inputs) == 0 { + return nil + } + if len(inputs) == 1 { + pr.api.AssertIsEqual(sel, 0) + return inputs[0] + } + var ret GT + D0C0B0A0 := make([]frontend.Variable, len(inputs)) + D0C0B0A1 := make([]frontend.Variable, len(inputs)) + D0C0B1A0 := make([]frontend.Variable, len(inputs)) + D0C0B1A1 := make([]frontend.Variable, len(inputs)) + D0C1B0A0 := make([]frontend.Variable, len(inputs)) + D0C1B0A1 := make([]frontend.Variable, len(inputs)) + D0C1B1A0 := make([]frontend.Variable, len(inputs)) + D0C1B1A1 := make([]frontend.Variable, len(inputs)) + D0C2B0A0 := make([]frontend.Variable, len(inputs)) + D0C2B0A1 := make([]frontend.Variable, len(inputs)) + D0C2B1A0 := make([]frontend.Variable, len(inputs)) + D0C2B1A1 := make([]frontend.Variable, len(inputs)) + D1C0B0A0 := make([]frontend.Variable, len(inputs)) + D1C0B0A1 := make([]frontend.Variable, len(inputs)) + D1C0B1A0 := make([]frontend.Variable, len(inputs)) + D1C0B1A1 := make([]frontend.Variable, len(inputs)) + D1C1B0A0 := make([]frontend.Variable, len(inputs)) + D1C1B0A1 := make([]frontend.Variable, len(inputs)) + D1C1B1A0 := make([]frontend.Variable, len(inputs)) + D1C1B1A1 := make([]frontend.Variable, len(inputs)) + D1C2B0A0 := make([]frontend.Variable, len(inputs)) + D1C2B0A1 := make([]frontend.Variable, len(inputs)) + D1C2B1A0 := make([]frontend.Variable, len(inputs)) + D1C2B1A1 := make([]frontend.Variable, len(inputs)) + for i := range inputs { + D0C0B0A0[i] = inputs[i].D0.C0.B0.A0 + D0C0B0A1[i] = inputs[i].D0.C0.B0.A1 + D0C0B1A0[i] = inputs[i].D0.C0.B1.A0 + D0C0B1A1[i] = inputs[i].D0.C0.B1.A1 + D0C1B0A0[i] = inputs[i].D0.C1.B0.A0 + D0C1B0A1[i] = inputs[i].D0.C1.B0.A1 + D0C1B1A0[i] = inputs[i].D0.C1.B1.A0 + D0C1B1A1[i] = inputs[i].D0.C1.B1.A1 + D0C2B0A0[i] = inputs[i].D0.C2.B0.A0 + D0C2B0A1[i] = inputs[i].D0.C2.B0.A1 + D0C2B1A0[i] = inputs[i].D0.C2.B1.A0 + D0C2B1A1[i] = inputs[i].D0.C2.B1.A1 + D1C0B0A0[i] = inputs[i].D1.C0.B0.A0 + D1C0B0A1[i] = inputs[i].D1.C0.B0.A1 + D1C0B1A0[i] = inputs[i].D1.C0.B1.A0 + D1C0B1A1[i] = inputs[i].D1.C0.B1.A1 + D1C1B0A0[i] = inputs[i].D1.C1.B0.A0 + D1C1B0A1[i] = inputs[i].D1.C1.B0.A1 + D1C1B1A0[i] = inputs[i].D1.C1.B1.A0 + D1C1B1A1[i] = inputs[i].D1.C1.B1.A1 + D1C2B0A0[i] = inputs[i].D1.C2.B0.A0 + D1C2B0A1[i] = inputs[i].D1.C2.B0.A1 + D1C2B1A0[i] = inputs[i].D1.C2.B1.A0 + D1C2B1A1[i] = inputs[i].D1.C2.B1.A1 + } + ret.D0.C0.B0.A0 = selector.Mux(pr.api, sel, D0C0B0A0...) + ret.D0.C0.B0.A1 = selector.Mux(pr.api, sel, D0C0B0A1...) + ret.D0.C0.B1.A0 = selector.Mux(pr.api, sel, D0C0B1A0...) + ret.D0.C0.B1.A1 = selector.Mux(pr.api, sel, D0C0B1A1...) + ret.D0.C1.B0.A0 = selector.Mux(pr.api, sel, D0C1B0A0...) + ret.D0.C1.B0.A1 = selector.Mux(pr.api, sel, D0C1B0A1...) + ret.D0.C1.B1.A0 = selector.Mux(pr.api, sel, D0C1B1A0...) + ret.D0.C1.B1.A1 = selector.Mux(pr.api, sel, D0C1B1A1...) + ret.D0.C2.B0.A0 = selector.Mux(pr.api, sel, D0C2B0A0...) + ret.D0.C2.B0.A1 = selector.Mux(pr.api, sel, D0C2B0A1...) + ret.D0.C2.B1.A0 = selector.Mux(pr.api, sel, D0C2B1A0...) + ret.D0.C2.B1.A1 = selector.Mux(pr.api, sel, D0C2B1A1...) + ret.D1.C0.B0.A0 = selector.Mux(pr.api, sel, D1C0B0A0...) + ret.D1.C0.B0.A1 = selector.Mux(pr.api, sel, D1C0B0A1...) + ret.D1.C0.B1.A0 = selector.Mux(pr.api, sel, D1C0B1A0...) + ret.D1.C0.B1.A1 = selector.Mux(pr.api, sel, D1C0B1A1...) + ret.D1.C1.B0.A0 = selector.Mux(pr.api, sel, D1C1B0A0...) + ret.D1.C1.B0.A1 = selector.Mux(pr.api, sel, D1C1B0A1...) + ret.D1.C1.B1.A0 = selector.Mux(pr.api, sel, D1C1B1A0...) + ret.D1.C1.B1.A1 = selector.Mux(pr.api, sel, D1C1B1A1...) + ret.D1.C2.B0.A0 = selector.Mux(pr.api, sel, D1C2B0A0...) + ret.D1.C2.B0.A1 = selector.Mux(pr.api, sel, D1C2B0A1...) + ret.D1.C2.B1.A0 = selector.Mux(pr.api, sel, D1C2B1A0...) + ret.D1.C2.B1.A1 = selector.Mux(pr.api, sel, D1C2B1A1...) + + return &ret +} func (p *Pairing) AssertIsOnG1(P *G1Affine) { panic("not implemented") diff --git a/std/algebra/native/sw_bls24315/pairing2_test.go b/std/algebra/native/sw_bls24315/pairing2_test.go index e7fbb989..1aebbed5 100644 --- a/std/algebra/native/sw_bls24315/pairing2_test.go +++ b/std/algebra/native/sw_bls24315/pairing2_test.go @@ -2,6 +2,7 @@ package sw_bls24315 import ( "crypto/rand" + "fmt" "math/big" "testing" @@ -12,6 +13,24 @@ import ( "github.com/consensys/gnark/test" ) +func randomG1G2Affines() (bls24315.G1Affine, bls24315.G2Affine) { + _, _, G1AffGen, G2AffGen := bls24315.Generators() + mod := bls24315.ID.ScalarField() + s1, err := rand.Int(rand.Reader, mod) + if err != nil { + panic(err) + } + s2, err := rand.Int(rand.Reader, mod) + if err != nil { + panic(err) + } + var p bls24315.G1Affine + p.ScalarMultiplication(&G1AffGen, s1) + var q bls24315.G2Affine + q.ScalarMultiplication(&G2AffGen, s2) + return p, q +} + type MuxCircuitTest struct { Selector frontend.Variable Inputs [8]G1Affine @@ -53,3 +72,91 @@ func TestMux(t *testing.T) { err := test.IsSolved(&circuit, &witness, ecc.BW6_761.ScalarField()) assert.NoError(err) } + +type MuxG2GtCircuit struct { + InG2 []G2Affine + InGt []GT + SelG2 frontend.Variable + SelGt frontend.Variable + ExpectedG2 G2Affine + ExpectedGt GT +} + +func (c *MuxG2GtCircuit) Define(api frontend.API) error { + pairing := NewPairing(api) + var inG2 []*G2Affine + for i := range c.InG2 { + inG2 = append(inG2, &c.InG2[i]) + } + var inGt []*GT + for i := range c.InGt { + inGt = append(inGt, &c.InGt[i]) + } + g2 := pairing.MuxG2(c.SelG2, inG2...) + gt := pairing.MuxGt(c.SelGt, inGt...) + if len(c.InG2) == 0 { + if g2 != nil { + return fmt.Errorf("mux G2: expected nil, got %v", g2) + } + } else { + c.ExpectedG2.P.AssertIsEqual(api, g2.P) + } + if len(c.InGt) == 0 { + if gt != nil { + return fmt.Errorf("mux Gt: expected nil, got %v", gt) + } + } else { + pairing.AssertIsEqual(gt, &c.ExpectedGt) + } + return nil +} + +func TestPairingMuxes(t *testing.T) { + assert := test.NewAssert(t) + var err error + for _, nbPairs := range []int{0, 1, 2, 3, 4, 5} { + assert.Run(func(assert *test.Assert) { + g2s := make([]bls24315.G2Affine, nbPairs) + gts := make([]bls24315.GT, nbPairs) + var p bls24315.G1Affine + witG2s := make([]G2Affine, nbPairs) + witGts := make([]GT, nbPairs) + for i := range nbPairs { + p, g2s[i] = randomG1G2Affines() + gts[i], err = bls24315.Pair([]bls24315.G1Affine{p}, []bls24315.G2Affine{g2s[i]}) + assert.NoError(err) + witG2s[i] = NewG2Affine(g2s[i]) + witGts[i] = NewGTEl(gts[i]) + } + circuit := MuxG2GtCircuit{InG2: make([]G2Affine, nbPairs), InGt: make([]GT, nbPairs)} + var witness MuxG2GtCircuit + if nbPairs > 0 { + selG2, err := rand.Int(rand.Reader, big.NewInt(int64(nbPairs))) + assert.NoError(err) + selGt, err := rand.Int(rand.Reader, big.NewInt(int64(nbPairs))) + assert.NoError(err) + expectedG2 := witG2s[selG2.Int64()] + expectedGt := witGts[selGt.Int64()] + witness = MuxG2GtCircuit{ + InG2: witG2s, + InGt: witGts, + SelG2: selG2, + SelGt: selGt, + ExpectedG2: expectedG2, + ExpectedGt: expectedGt, + } + } else { + witness = MuxG2GtCircuit{ + InG2: witG2s, + InGt: witGts, + SelG2: big.NewInt(0), + SelGt: big.NewInt(0), + ExpectedG2: NewG2Affine(bls24315.G2Affine{}), + ExpectedGt: NewGTEl(bls24315.GT{}), + } + } + err = test.IsSolved(&circuit, &witness, ecc.BW6_761.ScalarField()) + assert.NoError(err) + }, fmt.Sprintf("nbPairs=%d", nbPairs)) + } +} diff --git a/std/compress/internal/io.go b/std/compress/internal/io.go index c618df5f..a5eb3804 100644 --- a/std/compress/internal/io.go +++ b/std/compress/internal/io.go @@ -2,22 +2,23 @@ package internal import ( "errors" + "math/big" + hint "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/std/compress" "github.com/consensys/gnark/std/compress/internal/plonk" "github.com/consensys/gnark/std/lookup/logderivlookup" - "math/big" ) // TODO Use std/rangecheck instead type RangeChecker struct { api frontend.API - tables map[uint]*logderivlookup.Table + tables map[uint]logderivlookup.Table } func NewRangeChecker(api frontend.API) *RangeChecker { - return &RangeChecker{api: api, tables: make(map[uint]*logderivlookup.Table)} + return &RangeChecker{api: api, tables: make(map[uint]logderivlookup.Table)} } func (r *RangeChecker) AssertLessThan(bound uint, c ...frontend.Variable) { @@ -146,3 +147,12 @@ func BreakUpBytesIntoCrumbsHint(_ *big.Int, ins, outs []*big.Int) error { func BreakUpBytesIntoHalfHint(_ *big.Int, ins, outs []*big.Int) error { // todo find catchy name for 4 bits return breakUpBytesIntoWords(4, ins, outs) } + +// TODO @Tabaie: useful util (equivalent function used in GKR package). Find a better home for it +func ToVariableSlice[T any](slice []T) []frontend.Variable { + res := make([]frontend.Variable, len(slice)) + for i := range slice { + res[i] = slice[i] + } + return res +} diff --git a/std/compress/internal/io_test.go b/std/compress/internal/io_test.go index f88b9cc0..cc0269d5 100644 --- a/std/compress/internal/io_test.go +++ b/std/compress/internal/io_test.go @@ -12,7 +12,6 @@ import ( "github.com/consensys/gnark/std/compress" "github.com/consensys/gnark/std/compress/internal" "github.com/consensys/gnark/std/compress/lzss" - test_vector_utils "github.com/consensys/gnark/std/internal/test_vectors_utils" "github.com/consensys/gnark/std/math/bits" "github.com/consensys/gnark/test" "github.com/icza/bitio" @@ -50,9 +49,9 @@ func TestRecombineBytes(t *testing.T) { } assignment := recombineBytesCircuit{ - Bytes: test_vector_utils.ToVariableSlice(_bytes), - Bits: test_vector_utils.ToVariableSlice(bits), - Recombined: test_vector_utils.ToVariableSlice(recombined), + Bytes: internal.ToVariableSlice(_bytes), + Bits: internal.ToVariableSlice(bits), + Recombined: internal.ToVariableSlice(recombined), } lzss.RegisterHints() diff --git a/std/compress/internal/plonk/plonk_test.go b/std/compress/internal/plonk/plonk_test.go index 7392139f..4188654d 100644 --- a/std/compress/internal/plonk/plonk_test.go +++ b/std/compress/internal/plonk/plonk_test.go @@ -1,4 +1,4 @@ -package plonk +package plonk_test import ( "crypto/rand" @@ -7,11 +7,13 @@ import ( "reflect" "testing" + "github.com/consensys/gnark/std/compress/internal" + "github.com/consensys/gnark/std/compress/internal/plonk" + "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" "github.com/consensys/gnark/backend" "github.com/consensys/gnark/frontend" - test_vector_utils "github.com/consensys/gnark/std/internal/test_vectors_utils" "github.com/consensys/gnark/test" ) @@ -65,8 +67,8 @@ func TestCustomConstraint(t *testing.T) { circuit.oVal[i] = sum } - assignment.A = test_vector_utils.ToVariableSlice(circuit.aVal) - assignment.B = test_vector_utils.ToVariableSlice(circuit.bVal) + assignment.A = internal.ToVariableSlice(circuit.aVal) + assignment.B = internal.ToVariableSlice(circuit.bVal) test.NewAssert(t).CheckCircuit(&circuit, test.WithValidAssignment(&assignment), test.WithBackends(backend.PLONK), test.WithCurves(ecc.BLS12_377)) } @@ -123,10 +125,10 @@ func (c *customConstraintCircuit) Define(api frontend.API) error { for i := range c.A { a, b, o := ifConstThenElse(api, c.mode[i]&1, c.aVal[i], c.A[i]), ifConstThenElse(api, c.mode[i]&2, c.bVal[i], c.B[i]), ifConstThenElse(api, c.mode[i]&4, c.oVal[i], c.O[i]) - _o := EvaluateExpression(api, a, b, c.qL[i], c.qR[i], c.qM[i], c.qC[i]) + _o := plonk.EvaluateExpression(api, a, b, c.qL[i], c.qR[i], c.qM[i], c.qC[i]) api.AssertIsEqual(_o, o) - AddConstraint(api, a, b, o, c.qL[i], c.qR[i], -1, c.qM[i], c.qC[i]) + plonk.AddConstraint(api, a, b, o, c.qL[i], c.qR[i], -1, c.qM[i], c.qC[i]) } return nil diff --git a/std/compress/io.go b/std/compress/io.go index bc7a8486..25dadfae 100644 --- a/std/compress/io.go +++ b/std/compress/io.go @@ -2,12 +2,13 @@ package compress import ( "errors" + "hash" + "math/big" + "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/std/compress/internal/plonk" "github.com/consensys/gnark/std/hash/mimc" "github.com/consensys/gnark/std/lookup/logderivlookup" - "hash" - "math/big" ) // Pack packs the words as tightly as possible, and works Big Endian: i.e. the first word is the most significant in the packed elem diff --git a/std/compress/io_test.go b/std/compress/io_test.go index ea07eeb1..e42c8145 100644 --- a/std/compress/io_test.go +++ b/std/compress/io_test.go @@ -1,4 +1,4 @@ -package compress +package compress_test import ( "crypto/rand" @@ -7,6 +7,9 @@ import ( "math/big" "testing" + "github.com/consensys/gnark/std/compress" + "github.com/consensys/gnark/std/compress/internal" + "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" "github.com/consensys/gnark-crypto/hash" @@ -14,7 +17,6 @@ import ( "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/profile" - test_vector_utils "github.com/consensys/gnark/std/internal/test_vectors_utils" "github.com/consensys/gnark/test" "github.com/stretchr/testify/assert" ) @@ -43,8 +45,8 @@ func TestShiftLeft(t *testing.T) { } assignment := shiftLeftCircuit{ - Slice: test_vector_utils.ToVariableSlice(b), - Shifted: test_vector_utils.ToVariableSlice(shifted), + Slice: internal.ToVariableSlice(b), + Shifted: internal.ToVariableSlice(shifted), ShiftAmount: shiftAmount, } @@ -77,7 +79,7 @@ func (c *shiftLeftCircuit) Define(api frontend.API) error { if len(c.Slice) != len(c.Shifted) { panic("witness length mismatch") } - shifted := ShiftLeft(api, c.Slice, c.ShiftAmount) + shifted := compress.ShiftLeft(api, c.Slice, c.ShiftAmount) if len(shifted) != len(c.Shifted) { panic("wrong length") } @@ -94,14 +96,14 @@ func TestChecksumBytes(t *testing.T) { _, err := rand.Read(b) assert.NoError(t, err) - checksum := ChecksumPaddedBytes(b, len(b), hash.MIMC_BLS12_377.New(), fr.Bits) + checksum := compress.ChecksumPaddedBytes(b, len(b), hash.MIMC_BLS12_377.New(), fr.Bits) circuit := checksumTestCircuit{ Bytes: make([]frontend.Variable, len(b)), } assignment := checksumTestCircuit{ - Bytes: test_vector_utils.ToVariableSlice(b), + Bytes: internal.ToVariableSlice(b), Sum: checksum, } @@ -116,8 +118,8 @@ type checksumTestCircuit struct { } func (c *checksumTestCircuit) Define(api frontend.API) error { - Packed := append(Pack(api, c.Bytes, 8), len(c.Bytes)) - return AssertChecksumEquals(api, Packed, c.Sum) + Packed := append(compress.Pack(api, c.Bytes, 8), len(c.Bytes)) + return compress.AssertChecksumEquals(api, Packed, c.Sum) } func TestSetNumNbBits(t *testing.T) { @@ -132,8 +134,8 @@ func TestSetNumNbBits(t *testing.T) { test.WithCurves(ecc.BLS12_377), test.WithBackends(backend.PLONK), test.WithValidAssignment(&testSetNumNbBitsCircuit{ increases: increases, - Words: test_vector_utils.ToVariableSlice(words), - Nums: test_vector_utils.ToVariableSlice(nums), + Words: internal.ToVariableSlice(words), + Nums: internal.ToVariableSlice(nums), })) } @@ -195,7 +197,7 @@ func (c *testSetNumNbBitsCircuit) Define(api frontend.API) error { return errors.New("must have as many steps as read values") } l := 1 - nr := NewNumReader(api, c.Words, l, 1) + nr := compress.NewNumReader(api, c.Words, l, 1) for i := range c.increases { l += int(c.increases[i]) nr.SetNumNbBits(l) diff --git a/std/compress/lzss/large-tests/main.go b/std/compress/lzss/large-tests/main.go index 84f19f2f..caf2a1c2 100644 --- a/std/compress/lzss/large-tests/main.go +++ b/std/compress/lzss/large-tests/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/scs" diff --git a/std/compress/lzss/snark.go b/std/compress/lzss/snark.go index 68b9773b..430dbd53 100644 --- a/std/compress/lzss/snark.go +++ b/std/compress/lzss/snark.go @@ -136,7 +136,7 @@ func Decompress(api frontend.API, c []frontend.Variable, cLength frontend.Variab return dLength, nil } -func sliceToTable(api frontend.API, slice []frontend.Variable) *logderivlookup.Table { +func sliceToTable(api frontend.API, slice []frontend.Variable) logderivlookup.Table { table := logderivlookup.New(api) for i := range slice { table.Insert(slice[i]) @@ -145,7 +145,7 @@ func sliceToTable(api frontend.API, slice []frontend.Variable) *logderivlookup.T } // the "address" is zero when we don't have a backref delimiter -func initAddrTable(api frontend.API, bytes, _bits []frontend.Variable, backRefs ...lzss.BackrefType) *logderivlookup.Table { +func initAddrTable(api frontend.API, bytes, _bits []frontend.Variable, backRefs ...lzss.BackrefType) logderivlookup.Table { if len(backRefs) != 2 { panic("two backref types are expected, due to opts at the end of the function") } diff --git a/std/compress/lzss/snark_test.go b/std/compress/lzss/snark_test.go index 34936dd7..f9af5635 100644 --- a/std/compress/lzss/snark_test.go +++ b/std/compress/lzss/snark_test.go @@ -7,13 +7,14 @@ import ( "os" "testing" + "github.com/consensys/gnark/std/compress/internal" + "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/compress/lzss" "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/backend" "github.com/consensys/gnark/frontend" - test_vector_utils "github.com/consensys/gnark/std/internal/test_vectors_utils" "github.com/consensys/gnark/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -132,8 +133,8 @@ func TestNoCompression(t *testing.T) { CheckCorrectness: true, } assignment := &DecompressionTestCircuit{ - C: test_vector_utils.ToVariableSlice(append(c, make([]byte, inputExtraBytes)...)), - D: test_vector_utils.ToVariableSlice(d), + C: internal.ToVariableSlice(append(c, make([]byte, inputExtraBytes)...)), + D: internal.ToVariableSlice(d), CBegin: 0, CLength: len(c), DLength: len(d), @@ -175,8 +176,8 @@ func Test3c2943withHeader(t *testing.T) { CheckCorrectness: true, } assignment := &DecompressionTestCircuit{ - C: test_vector_utils.ToVariableSlice(append(c, make([]byte, inputExtraBytes)...)), - D: test_vector_utils.ToVariableSlice(d), + C: internal.ToVariableSlice(append(c, make([]byte, inputExtraBytes)...)), + D: internal.ToVariableSlice(d), CBegin: 10, CLength: len(c) - 10, DLength: len(d), @@ -201,9 +202,9 @@ func TestOutBufTooShort(t *testing.T) { } assignment := decompressionLengthTestCircuit{ - C: test_vector_utils.ToVariableSlice(append(c, make([]byte, inputExtraBytes)...)), + C: internal.ToVariableSlice(append(c, make([]byte, inputExtraBytes)...)), CLength: len(c), - D: test_vector_utils.ToVariableSlice(d[:len(d)-truncationAmount]), + D: internal.ToVariableSlice(d[:len(d)-truncationAmount]), ExpectedDLength: -1, } @@ -315,8 +316,8 @@ func testCompressionRoundTrip(t *testing.T, d, dict []byte, options ...testCompr CheckCorrectness: true, } assignment := &DecompressionTestCircuit{ - C: test_vector_utils.ToVariableSlice(append(s.compressed, make([]byte, s.compressedPaddingLen)...)), - D: test_vector_utils.ToVariableSlice(d), + C: internal.ToVariableSlice(append(s.compressed, make([]byte, s.compressedPaddingLen)...)), + D: internal.ToVariableSlice(d), CBegin: s.cBegin, CLength: len(s.compressed), DLength: len(d), @@ -341,7 +342,7 @@ type decompressionLengthTestCircuit struct { } func (c *decompressionLengthTestCircuit) Define(api frontend.API) error { - dict := test_vector_utils.ToVariableSlice(lzss.AugmentDict(nil)) + dict := internal.ToVariableSlice(lzss.AugmentDict(nil)) if dLength, err := Decompress(api, c.C, c.CLength, c.D, dict); err != nil { return err } else { diff --git a/std/compress/lzss/snark_testing.go b/std/compress/lzss/snark_testing.go index fc309a3f..09900c06 100644 --- a/std/compress/lzss/snark_testing.go +++ b/std/compress/lzss/snark_testing.go @@ -4,7 +4,7 @@ import ( "github.com/consensys/compress/lzss" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/std/compress" - test_vector_utils "github.com/consensys/gnark/std/internal/test_vectors_utils" + "github.com/consensys/gnark/std/compress/internal" ) type DecompressionTestCircuit struct { @@ -18,7 +18,7 @@ type DecompressionTestCircuit struct { } func (c *DecompressionTestCircuit) Define(api frontend.API) error { - dict := test_vector_utils.ToVariableSlice(lzss.AugmentDict(c.Dict)) + dict := internal.ToVariableSlice(lzss.AugmentDict(c.Dict)) dBack := make([]frontend.Variable, len(c.D)) // TODO Try smaller constants if cb, ok := c.CBegin.(int); !ok || cb != 0 { c.C = compress.ShiftLeft(api, c.C, c.CBegin) diff --git a/std/evmprecompiles/06-bnadd.go b/std/evmprecompiles/06-bnadd.go index ff6c397f..242d5a98 100644 --- a/std/evmprecompiles/06-bnadd.go +++ b/std/evmprecompiles/06-bnadd.go @@ -14,7 +14,7 @@ func ECAdd(api frontend.API, P, Q *sw_emulated.AffinePoint[emulated.BN254Fp]) *s if err != nil { panic(err) } - // Check that P and Q are on the curve (done in the zkEVM ⚠️ ) + // Check that P and Q are on G1 (done in the zkEVM ⚠️ ) // We use AddUnified because P can be equal to Q, -Q and either or both can be (0,0) res := curve.AddUnified(P, Q) return res diff --git a/std/evmprecompiles/07-bnmul.go b/std/evmprecompiles/07-bnmul.go index eb1d0889..8aa32060 100644 --- a/std/evmprecompiles/07-bnmul.go +++ b/std/evmprecompiles/07-bnmul.go @@ -15,7 +15,7 @@ func ECMul(api frontend.API, P *sw_emulated.AffinePoint[emulated.BN254Fp], u *em if err != nil { panic(err) } - // Check that P is on the curve (done in the zkEVM ⚠️ ) + // Check that P is on G1 (done in the zkEVM ⚠️ ) res := curve.ScalarMul(P, u, algopts.WithCompleteArithmetic()) return res } diff --git a/std/evmprecompiles/08-bnpairing.go b/std/evmprecompiles/08-bnpairing.go index f4ff6b1b..ec635c99 100644 --- a/std/evmprecompiles/08-bnpairing.go +++ b/std/evmprecompiles/08-bnpairing.go @@ -40,8 +40,11 @@ func ECPair(api frontend.API, P []*sw_bn254.G1Affine, Q []*sw_bn254.G2Affine) { if err != nil { panic(err) } - // 1- Check that Pᵢ are on G1 (done in the zkEVM ⚠️ - // 2- Check that Qᵢ are on G2 (done in `computeLines` in `MillerLoopAndMul` and `MillerLoopAndFinalExpCheck) + // 1- Check that Pᵢ are on G1 (done in the zkEVM ⚠️) + // N.B.: BN254 has a prime order so G1 membership boils down to curve + // membership only, which is checked in the zkEVM. + // + // 2- Check that Qᵢ are on G2 (done in `computeLines` in `MillerLoopAndMul` and `MillerLoopAndFinalExpCheck`) // 3- Check that ∏ᵢ e(Pᵢ, Qᵢ) == 1 ml := pair.Ext12.One() diff --git a/std/evmprecompiles/11-blsg1add.go b/std/evmprecompiles/11-blsg1add.go new file mode 100644 index 00000000..20c4a19e --- /dev/null +++ b/std/evmprecompiles/11-blsg1add.go @@ -0,0 +1,25 @@ +package evmprecompiles + +import ( + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/emulated/sw_emulated" + "github.com/consensys/gnark/std/math/emulated" +) + +// ECAddG1BLS implements [BLS12_G1ADD] precompile contract at address 0x0b. +// +// [BLS12_G1ADD]: https://eips.ethereum.org/EIPS/eip-2537 +func ECAddG1BLS(api frontend.API, P, Q *sw_emulated.AffinePoint[emulated.BLS12381Fp]) *sw_emulated.AffinePoint[emulated.BLS12381Fp] { + curve, err := sw_emulated.New[emulated.BLS12381Fp, emulated.BLS12381Fr](api, sw_emulated.GetBLS12381Params()) + if err != nil { + panic(err) + } + // Check that P and Q are on curve + // N.B.: There is no subgroup check for the G1 addition precompile. + curve.AssertIsOnCurve(P) + curve.AssertIsOnCurve(Q) + + // We use AddUnified because P can be equal to Q, -Q and either or both can be (0,0) + res := curve.AddUnified(P, Q) + return res +} diff --git a/std/evmprecompiles/12-blsg1msm.go b/std/evmprecompiles/12-blsg1msm.go new file mode 100644 index 00000000..8508487c --- /dev/null +++ b/std/evmprecompiles/12-blsg1msm.go @@ -0,0 +1,36 @@ +package evmprecompiles + +import ( + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/algopts" + "github.com/consensys/gnark/std/algebra/emulated/sw_bls12381" + "github.com/consensys/gnark/std/algebra/emulated/sw_emulated" + "github.com/consensys/gnark/std/math/emulated" +) + +// ECMSMG1BLS implements [BLS12_G1MSM] precompile contract at address 0x0c. +// +// [BLS12_G1MSM]: https://eips.ethereum.org/EIPS/eip-2537 +func ECMSMG1BLS(api frontend.API, P []*sw_emulated.AffinePoint[emulated.BLS12381Fp], s []*emulated.Element[emulated.BLS12381Fr]) *sw_emulated.AffinePoint[emulated.BLS12381Fp] { + curve, err := sw_emulated.New[emulated.BLS12381Fp, emulated.BLS12381Fr](api, sw_emulated.GetBLS12381Params()) + if err != nil { + panic(err) + } + g1, err := sw_bls12381.NewG1(api) + if err != nil { + panic(err) + } + + // Check that Pᵢ are on G1 + for _, p := range P { + g1.AssertIsOnG1(p) + } + + // Compute the MSM + res, err := curve.MultiScalarMul(P, s, algopts.WithCompleteArithmetic()) + if err != nil { + panic(err) + } + + return res +} diff --git a/std/evmprecompiles/13-blsg2add.go b/std/evmprecompiles/13-blsg2add.go new file mode 100644 index 00000000..776f1aa1 --- /dev/null +++ b/std/evmprecompiles/13-blsg2add.go @@ -0,0 +1,25 @@ +package evmprecompiles + +import ( + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/emulated/sw_bls12381" +) + +// ECAddG2BLS implements [BLS12_G2ADD] precompile contract at address 0x0d. +// +// [BLS12_G2ADD]: https://eips.ethereum.org/EIPS/eip-2537 +func ECAddG2BLS(api frontend.API, P, Q *sw_bls12381.G2Affine) *sw_bls12381.G2Affine { + g2, err := sw_bls12381.NewG2(api) + if err != nil { + panic(err) + } + + // Check that P and Q are on curve + // N.B.: There is no subgroup check for the G2 addition precompile. + g2.AssertIsOnTwist(P) + g2.AssertIsOnTwist(Q) + + // We use AddUnified because P can be equal to Q, -Q and either or both can be (0,0) + res := g2.AddUnified(P, Q) + return res +} diff --git a/std/evmprecompiles/14-blsg2msm.go b/std/evmprecompiles/14-blsg2msm.go new file mode 100644 index 00000000..e502e329 --- /dev/null +++ b/std/evmprecompiles/14-blsg2msm.go @@ -0,0 +1,31 @@ +package evmprecompiles + +import ( + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/algopts" + "github.com/consensys/gnark/std/algebra/emulated/sw_bls12381" + "github.com/consensys/gnark/std/math/emulated" +) + +// ECMSMG2BLS implements [BLS12_G2MSM] precompile contract at address 0x0e. +// +// [BLS12_G2MSM]: https://eips.ethereum.org/EIPS/eip-2537 +func ECMSMG2BLS(api frontend.API, P []*sw_bls12381.G2Affine, s []*emulated.Element[emulated.BLS12381Fr]) *sw_bls12381.G2Affine { + g2, err := sw_bls12381.NewG2(api) + if err != nil { + panic(err) + } + + // Check that Pᵢ are on G2 + for _, p := range P { + g2.AssertIsOnG2(p) + } + + // Compute the MSM + res, err := g2.MultiScalarMul(P, s, algopts.WithCompleteArithmetic()) + if err != nil { + panic(err) + } + + return res +} diff --git a/std/evmprecompiles/15-blspairing.go b/std/evmprecompiles/15-blspairing.go new file mode 100644 index 00000000..c1337815 --- /dev/null +++ b/std/evmprecompiles/15-blspairing.go @@ -0,0 +1,118 @@ +package evmprecompiles + +import ( + "fmt" + + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/emulated/sw_bls12381" +) + +// ECPairBLS implements [BLS12_PAIRING_CHECK] precompile contract at address 0x0f. +// +// To have a fixed-circuit regardless of the number of inputs, we need 2 fixed circuits: +// - MillerLoopAndMul: +// A Miller loop of fixed size 1 followed by a multiplication in 𝔽p¹². +// - MillerLoopAndFinalExpCheck: +// A Miller loop of fixed size 1 followed by a multiplication in 𝔽p¹², and +// a check that the result lies in the same equivalence class as the +// reduced pairing purported to be 1. This check replaces the final +// exponentiation step in-circuit and follows Section 4 of [On Proving +// Pairings] paper by A. Novakovic and L. Eagen. +// +// N.B.: This is a sub-optimal routine but defines a fixed circuit regardless +// of the number of inputs. We can extend this routine to handle a 2-by-2 +// logic but we prefer a minimal number of circuits (2). +// +// See the methods [ECPairMillerLoopAndMul] and [ECPairMillerLoopAndFinalExpCheck] for the fixed circuits. +// See the methods [ECPairBLSIsOnG1] and [ECPairBLSIsOnG2] for the check that Pᵢ and Qᵢ are on G1 and resp. G2. +// +// [BLS12_PAIRING_CHECK]: https://eips.ethereum.org/EIPS/eip-2537 +// [On Proving Pairings]: https://eprint.iacr.org/2024/640.pdf +func ECPairBLS(api frontend.API, P []*sw_bls12381.G1Affine, Q []*sw_bls12381.G2Affine) { + if len(P) != len(Q) { + panic("P and Q length mismatch") + } + if len(P) < 2 { + panic("invalid multipairing size bound") + } + n := len(P) + pair, err := sw_bls12381.NewPairing(api) + if err != nil { + panic(err) + } + for i := 0; i < n; i++ { + // 1- Check that Pᵢ are on G1 + pair.AssertIsOnG1(P[i]) + // N.B.: curve membership cannot be done in the zkEVM for BLS12-381 + // because the prime occupies 48 bytes and the zkEVM modular arithmetic + // module only supports 32 byte operations. + // + // 2- Check that Qᵢ are on G2 (done in `computeLines` in `MillerLoopAndMul` and `MillerLoopAndFinalExpCheck`) + } + + // 3- Check that ∏ᵢ e(Pᵢ, Qᵢ) == 1 + ml := pair.Ext12.One() + for i := 0; i < n-1; i++ { + // fixed circuit 1 + ml, err = pair.MillerLoopAndMul(P[i], Q[i], ml) + if err != nil { + panic(err) + } + } + + // fixed circuit 2 + pair.AssertMillerLoopAndFinalExpIsOne(P[n-1], Q[n-1], ml) +} + +// ECPairBLSIsOnG2 implements the fixed circuit for checking G2 membership and non-membership. +func ECPairBLSIsOnG2(api frontend.API, Q *sw_bls12381.G2Affine, expectedIsOnG2 frontend.Variable) error { + pairing, err := sw_bls12381.NewPairing(api) + if err != nil { + return err + } + isOnG2 := pairing.IsOnG2(Q) + api.AssertIsEqual(expectedIsOnG2, isOnG2) + return nil +} + +// ECPairBLSIsOnG1 implements the fixed circuit for checking G1 membership and non-membership. +func ECPairBLSIsOnG1(api frontend.API, Q *sw_bls12381.G1Affine, expectedIsOnG1 frontend.Variable) error { + pairing, err := sw_bls12381.NewPairing(api) + if err != nil { + return err + } + isOnG1 := pairing.IsOnG1(Q) + api.AssertIsEqual(expectedIsOnG1, isOnG1) + return nil +} + +// ECPairMillerLoopAndMul implements the fixed circuit for a Miller loop of +// fixed size 1 followed by a multiplication with an accumulator in 𝔽p¹². It +// asserts that the result corresponds to the expected result. +func ECPairBLSMillerLoopAndMul(api frontend.API, accumulator *sw_bls12381.GTEl, P *sw_bls12381.G1Affine, Q *sw_bls12381.G2Affine, expected *sw_bls12381.GTEl) error { + pairing, err := sw_bls12381.NewPairing(api) + if err != nil { + return fmt.Errorf("new pairing: %w", err) + } + ml, err := pairing.MillerLoopAndMul(P, Q, accumulator) + if err != nil { + return fmt.Errorf("miller loop and mul: %w", err) + } + pairing.AssertIsEqual(expected, ml) + return nil +} + +// ECPairMillerLoopAndFinalExpCheck implements the fixed circuit for a Miller +// loop of fixed size 1 followed by a multiplication with an accumulator in +// 𝔽p¹², and a check that the result corresponds to the expected result. +func ECPairBLSMillerLoopAndFinalExpCheck(api frontend.API, accumulator *sw_bls12381.GTEl, P *sw_bls12381.G1Affine, Q *sw_bls12381.G2Affine, expectedIsSuccess frontend.Variable) error { + api.AssertIsBoolean(expectedIsSuccess) + pairing, err := sw_bls12381.NewPairing(api) + if err != nil { + return fmt.Errorf("new pairing: %w", err) + } + + isSuccess := pairing.IsMillerLoopAndFinalExpOne(P, Q, accumulator) + api.AssertIsEqual(expectedIsSuccess, isSuccess) + return nil +} diff --git a/std/evmprecompiles/16-blsmaptog1.go b/std/evmprecompiles/16-blsmaptog1.go new file mode 100644 index 00000000..13a21365 --- /dev/null +++ b/std/evmprecompiles/16-blsmaptog1.go @@ -0,0 +1,25 @@ +package evmprecompiles + +import ( + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/emulated/sw_bls12381" + "github.com/consensys/gnark/std/algebra/emulated/sw_emulated" + "github.com/consensys/gnark/std/math/emulated" +) + +// ECMapToG1BLS implements [BLS12_MAP_FP_TO_G1] precompile contract at address 0x10. +// +// [ECMapToG1BLS]: https://eips.ethereum.org/EIPS/eip-2537 +func ECMapToG1BLS(api frontend.API, u *emulated.Element[emulated.BLS12381Fp]) *sw_emulated.AffinePoint[emulated.BLS12381Fp] { + g, err := sw_bls12381.NewG1(api) + if err != nil { + panic(err) + } + res, err := g.MapToG1(u) + if err != nil { + panic(err) + } + + return res + +} diff --git a/std/evmprecompiles/17-blsmaptog2.go b/std/evmprecompiles/17-blsmaptog2.go new file mode 100644 index 00000000..551620ca --- /dev/null +++ b/std/evmprecompiles/17-blsmaptog2.go @@ -0,0 +1,22 @@ +package evmprecompiles + +import ( + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/emulated/fields_bls12381" + "github.com/consensys/gnark/std/algebra/emulated/sw_bls12381" +) + +// ECMapToG2BLS implements [BLS12_MAP_FP2_TO_G2] precompile contract at address 0x11. +// +// [ECMapToG2BLS]: https://eips.ethereum.org/EIPS/eip-2537 +func ECMapToG2BLS(api frontend.API, u *fields_bls12381.E2) *sw_bls12381.G2Affine { + g, err := sw_bls12381.NewG2(api) + if err != nil { + panic(err) + } + res, err := g.MapToG2(u) + if err != nil { + panic(err) + } + return res +} diff --git a/std/evmprecompiles/bls_test.go b/std/evmprecompiles/bls_test.go new file mode 100644 index 00000000..a1872d62 --- /dev/null +++ b/std/evmprecompiles/bls_test.go @@ -0,0 +1,377 @@ +package evmprecompiles + +import ( + "fmt" + "math/big" + "testing" + + "github.com/consensys/gnark-crypto/ecc" + bls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fp" + "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/emulated/fields_bls12381" + "github.com/consensys/gnark/std/algebra/emulated/sw_bls12381" + "github.com/consensys/gnark/std/algebra/emulated/sw_emulated" + "github.com/consensys/gnark/std/math/emulated" + "github.com/consensys/gnark/test" +) + +// 11: G1 Add +type ecaddG1BLSCircuit struct { + X0 sw_emulated.AffinePoint[emulated.BLS12381Fp] + X1 sw_emulated.AffinePoint[emulated.BLS12381Fp] + Expected sw_emulated.AffinePoint[emulated.BLS12381Fp] +} + +func (c *ecaddG1BLSCircuit) Define(api frontend.API) error { + curve, err := sw_emulated.New[emulated.BLS12381Fp, emulated.BLS12381Fr](api, sw_emulated.GetBLS12381Params()) + if err != nil { + return err + } + res := ECAddG1BLS(api, &c.X0, &c.X1) + curve.AssertIsEqual(res, &c.Expected) + return nil +} + +func testRoutineECAddG1BLS() (circ, wit frontend.Circuit) { + _, _, G, _ := bls12381.Generators() + var u, v fr.Element + u.SetRandom() + v.SetRandom() + var P, Q bls12381.G1Affine + P.ScalarMultiplication(&G, u.BigInt(new(big.Int))) + Q.ScalarMultiplication(&G, v.BigInt(new(big.Int))) + var expected bls12381.G1Affine + expected.Add(&P, &Q) + circuit := ecaddG1BLSCircuit{} + witness := ecaddG1BLSCircuit{ + X0: sw_emulated.AffinePoint[emulated.BLS12381Fp]{ + X: emulated.ValueOf[emulated.BLS12381Fp](P.X), + Y: emulated.ValueOf[emulated.BLS12381Fp](P.Y), + }, + X1: sw_emulated.AffinePoint[emulated.BLS12381Fp]{ + X: emulated.ValueOf[emulated.BLS12381Fp](Q.X), + Y: emulated.ValueOf[emulated.BLS12381Fp](Q.Y), + }, + Expected: sw_emulated.AffinePoint[emulated.BLS12381Fp]{ + X: emulated.ValueOf[emulated.BLS12381Fp](expected.X), + Y: emulated.ValueOf[emulated.BLS12381Fp](expected.Y), + }, + } + return &circuit, &witness +} + +func TestECAddG1BLSCircuitShort(t *testing.T) { + assert := test.NewAssert(t) + circuit, witness := testRoutineECAddG1BLS() + err := test.IsSolved(circuit, witness, ecc.BN254.ScalarField()) + assert.NoError(err) +} + +func TestECAddG1BLSCircuitFull(t *testing.T) { + assert := test.NewAssert(t) + circuit, witness := testRoutineECAdd() + assert.CheckCircuit(circuit, test.WithValidAssignment(witness)) +} + +// 12: G1 MSM +type ecmsmg1BLSCircuit struct { + Points [10]sw_emulated.AffinePoint[emulated.BLS12381Fp] + Scalars [10]emulated.Element[emulated.BLS12381Fr] + Res sw_emulated.AffinePoint[emulated.BLS12381Fp] + n int +} + +func (c *ecmsmg1BLSCircuit) Define(api frontend.API) error { + curve, err := sw_emulated.New[emulated.BLS12381Fp, emulated.BLS12381Fr](api, sw_emulated.GetBLS12381Params()) + if err != nil { + return err + } + ps := make([]*sw_emulated.AffinePoint[emulated.BLS12381Fp], c.n) + for i := range c.n { + ps[i] = &c.Points[i] + } + ss := make([]*emulated.Element[emulated.BLS12381Fr], c.n) + for i := range c.n { + ss[i] = &c.Scalars[i] + } + res := ECMSMG1BLS(api, ps, ss) + curve.AssertIsEqual(res, &c.Res) + return nil +} + +func TestECMSMG1BLSCircuit(t *testing.T) { + assert := test.NewAssert(t) + P := make([]bls12381.G1Affine, 10) + S := make([]fr.Element, 10) + for i := 0; i < 10; i++ { + S[i].SetRandom() + P[i].ScalarMultiplicationBase(S[i].BigInt(new(big.Int))) + } + + var cP [10]sw_emulated.AffinePoint[emulated.BLS12381Fp] + for i := range cP { + cP[i] = sw_emulated.AffinePoint[emulated.BLS12381Fp]{ + X: emulated.ValueOf[emulated.BLS12381Fp](P[i].X), + Y: emulated.ValueOf[emulated.BLS12381Fp](P[i].Y), + } + } + var cS [10]emulated.Element[emulated.BLS12381Fr] + for i := range cS { + cS[i] = emulated.ValueOf[emulated.BLS12381Fr](S[i]) + } + + for i := 1; i < 11; i++ { + var res bls12381.G1Affine + _, err := res.MultiExp(P[:i], S[:i], ecc.MultiExpConfig{}) + assert.NoError(err) + err = test.IsSolved(&ecmsmg1BLSCircuit{n: i}, &ecmsmg1BLSCircuit{ + n: i, + Points: cP, + Scalars: cS, + Res: sw_emulated.AffinePoint[emulated.BLS12381Fp]{ + X: emulated.ValueOf[emulated.BLS12381Fp](res.X), + Y: emulated.ValueOf[emulated.BLS12381Fp](res.Y), + }, + }, ecc.BN254.ScalarField()) + assert.NoError(err) + } +} + +// 13: G2 Add +type ecaddG2BLSCircuit struct { + X0 sw_bls12381.G2Affine + X1 sw_bls12381.G2Affine + Expected sw_bls12381.G2Affine +} + +func (c *ecaddG2BLSCircuit) Define(api frontend.API) error { + g2, err := sw_bls12381.NewG2(api) + if err != nil { + panic(err) + } + res := ECAddG2BLS(api, &c.X0, &c.X1) + g2.AssertIsEqual(res, &c.Expected) + return nil +} + +func testRoutineECAddG2BLS() (circ, wit frontend.Circuit) { + _, _, _, G := bls12381.Generators() + var u, v fr.Element + u.SetRandom() + v.SetRandom() + var P, Q bls12381.G2Affine + P.ScalarMultiplication(&G, u.BigInt(new(big.Int))) + Q.ScalarMultiplication(&G, v.BigInt(new(big.Int))) + var expected bls12381.G2Affine + expected.Add(&P, &Q) + circuit := ecaddG2BLSCircuit{} + witness := ecaddG2BLSCircuit{ + X0: sw_bls12381.NewG2Affine(P), + X1: sw_bls12381.NewG2Affine(Q), + Expected: sw_bls12381.NewG2Affine(expected), + } + return &circuit, &witness +} + +func TestECAddG2BLSCircuitShort(t *testing.T) { + assert := test.NewAssert(t) + circuit, witness := testRoutineECAddG2BLS() + err := test.IsSolved(circuit, witness, ecc.BN254.ScalarField()) + assert.NoError(err) +} + +func TestECAddG2BLSCircuitFull(t *testing.T) { + assert := test.NewAssert(t) + circuit, witness := testRoutineECAddG2BLS() + assert.CheckCircuit(circuit, test.WithValidAssignment(witness)) +} + +// 14: G2 MSM +type ecmsmg2BLSCircuit struct { + Points [10]sw_bls12381.G2Affine + Scalars [10]sw_bls12381.Scalar + Res sw_bls12381.G2Affine + n int +} + +func (c *ecmsmg2BLSCircuit) Define(api frontend.API) error { + g2, err := sw_bls12381.NewG2(api) + if err != nil { + panic(err) + } + ps := make([]*sw_bls12381.G2Affine, c.n) + for i := range c.n { + ps[i] = &c.Points[i] + } + ss := make([]*sw_bls12381.Scalar, c.n) + for i := range c.n { + ss[i] = &c.Scalars[i] + } + res := ECMSMG2BLS(api, ps, ss) + g2.AssertIsEqual(res, &c.Res) + return nil +} + +func TestECMSMG2BLSCircuit(t *testing.T) { + assert := test.NewAssert(t) + P := make([]bls12381.G2Affine, 10) + S := make([]fr.Element, 10) + for i := 0; i < 10; i++ { + S[i].SetRandom() + P[i].ScalarMultiplicationBase(S[i].BigInt(new(big.Int))) + } + + var cP [10]sw_bls12381.G2Affine + for i := range cP { + cP[i] = sw_bls12381.NewG2Affine(P[i]) + } + var cS [10]emulated.Element[emulated.BLS12381Fr] + for i := range cS { + cS[i] = emulated.ValueOf[emulated.BLS12381Fr](S[i]) + } + + for i := 1; i < 11; i++ { + var res bls12381.G2Affine + _, err := res.MultiExp(P[:i], S[:i], ecc.MultiExpConfig{}) + assert.NoError(err) + err = test.IsSolved(&ecmsmg2BLSCircuit{n: i}, &ecmsmg2BLSCircuit{ + n: i, + Points: cP, + Scalars: cS, + Res: sw_bls12381.NewG2Affine(res), + }, ecc.BN254.ScalarField()) + assert.NoError(err) + } +} + +// 15: multi-pairing check +type ecPairBLSBatchCircuit struct { + P sw_bls12381.G1Affine + NP sw_bls12381.G1Affine + DP sw_bls12381.G1Affine + Q sw_bls12381.G2Affine + n int +} + +func (c *ecPairBLSBatchCircuit) Define(api frontend.API) error { + Q := make([]*sw_bls12381.G2Affine, c.n) + for i := range Q { + Q[i] = &c.Q + } + switch c.n { + case 2: + ECPairBLS(api, []*sw_emulated.AffinePoint[emulated.BLS12381Fp]{&c.P, &c.NP}, Q) + case 3: + ECPairBLS(api, []*sw_emulated.AffinePoint[emulated.BLS12381Fp]{&c.NP, &c.NP, &c.DP}, Q) + case 4: + ECPairBLS(api, []*sw_emulated.AffinePoint[emulated.BLS12381Fp]{&c.P, &c.NP, &c.P, &c.NP}, Q) + case 5: + ECPairBLS(api, []*sw_emulated.AffinePoint[emulated.BLS12381Fp]{&c.P, &c.NP, &c.NP, &c.NP, &c.DP}, Q) + case 6: + ECPairBLS(api, []*sw_emulated.AffinePoint[emulated.BLS12381Fp]{&c.P, &c.NP, &c.P, &c.NP, &c.P, &c.NP}, Q) + case 7: + ECPairBLS(api, []*sw_emulated.AffinePoint[emulated.BLS12381Fp]{&c.P, &c.NP, &c.P, &c.NP, &c.NP, &c.NP, &c.DP}, Q) + case 8: + ECPairBLS(api, []*sw_emulated.AffinePoint[emulated.BLS12381Fp]{&c.P, &c.NP, &c.P, &c.NP, &c.P, &c.NP, &c.P, &c.NP}, Q) + case 9: + ECPairBLS(api, []*sw_emulated.AffinePoint[emulated.BLS12381Fp]{&c.P, &c.NP, &c.P, &c.NP, &c.P, &c.NP, &c.NP, &c.NP, &c.DP}, Q) + default: + return fmt.Errorf("not handled %d", c.n) + } + return nil +} + +func TestECPairBLSBLSMulBatch(t *testing.T) { + assert := test.NewAssert(t) + _, _, p, q := bls12381.Generators() + + var u, v fr.Element + u.SetRandom() + v.SetRandom() + + p.ScalarMultiplication(&p, u.BigInt(new(big.Int))) + q.ScalarMultiplication(&q, v.BigInt(new(big.Int))) + + var dp, np bls12381.G1Affine + dp.Double(&p) + np.Neg(&p) + + for i := 2; i < 10; i++ { + err := test.IsSolved(&ecPairBLSBatchCircuit{n: i}, &ecPairBLSBatchCircuit{ + n: i, + P: sw_bls12381.NewG1Affine(p), + NP: sw_bls12381.NewG1Affine(np), + DP: sw_bls12381.NewG1Affine(dp), + Q: sw_bls12381.NewG2Affine(q), + }, ecc.BN254.ScalarField()) + assert.NoError(err) + } +} + +// 16: mapToG1 check +type eCMapToG1BLSCircuit struct { + A emulated.Element[emulated.BLS12381Fp] + R sw_bls12381.G1Affine +} + +func (c *eCMapToG1BLSCircuit) Define(api frontend.API) error { + + g, err := sw_bls12381.NewG1(api) + if err != nil { + return fmt.Errorf("new G1: %w", err) + } + r := ECMapToG1BLS(api, &c.A) + g.AssertIsEqual(r, &c.R) + + return nil +} + +func TestECMapToG1(t *testing.T) { + + assert := test.NewAssert(t) + var a fp.Element + a.SetRandom() + g := bls12381.MapToG1(a) + + witness := eCMapToG1BLSCircuit{ + A: emulated.ValueOf[emulated.BLS12381Fp](a.String()), + R: sw_bls12381.NewG1Affine(g), + } + + err := test.IsSolved(&eCMapToG1BLSCircuit{}, &witness, ecc.BN254.ScalarField()) + assert.NoError(err) +} + +type ECMapToG2BLSCircuit struct { + A fields_bls12381.E2 + R sw_bls12381.G2Affine +} + +func (c *ECMapToG2BLSCircuit) Define(api frontend.API) error { + g, err := sw_bls12381.NewG2(api) + if err != nil { + return fmt.Errorf("new G2: %w", err) + } + r := ECMapToG2BLS(api, &c.A) + g.AssertIsEqual(r, &c.R) + + return nil +} + +func TestECMapToG2(t *testing.T) { + assert := test.NewAssert(t) + var a bls12381.E2 + a.A0.SetRandom() + a.A1.SetRandom() + g := bls12381.MapToG2(a) + + witness := ECMapToG2BLSCircuit{ + A: fields_bls12381.FromE2(&a), + R: sw_bls12381.NewG2Affine(g), + } + + err := test.IsSolved(&ECMapToG2BLSCircuit{}, &witness, ecc.BN254.ScalarField()) + assert.NoError(err) +} diff --git a/std/evmprecompiles/doc.go b/std/evmprecompiles/doc.go index 9b7dc431..b96ff2a8 100644 --- a/std/evmprecompiles/doc.go +++ b/std/evmprecompiles/doc.go @@ -12,6 +12,14 @@ // 7. BN_MUL ✅ -- function [ECMul] // 8. SNARKV ✅ -- function [ECPair] // 9. BLAKE2F ❌ -- postponed +// 10. POINT_EVALUATION ❌ -- work in progress +// 11. BLS12_G1MSM ✅ -- function [ECAddG1BLS] +// 12. BLS12_G1MSM ✅ -- function [ECMSMG1BLS] +// 13. BLS12_G2ADD ✅ -- function [ECAddG2BLS] +// 14. BLS12_G2MSM ✅ -- function [ECMSMG2BLS] +// 15. BLS12_PAIRING_CHECK ✅ -- function [ECPairBLS] +// 16. BLS12_MAP_FP_TO_G1 ✅ -- function [ECMapToG1BLS] +// 17. BLS12_MAP_FP2_TO_G2 ✅ -- function [ECMapToG2BLS] // // This package uses local representation for the arguments. It is up to the // user to instantiate corresponding types from their application-specific data. diff --git a/std/gkr/api.go b/std/gkr/api.go deleted file mode 100644 index eb1acd2a..00000000 --- a/std/gkr/api.go +++ /dev/null @@ -1,45 +0,0 @@ -package gkr - -import ( - "github.com/consensys/gnark/constraint" - "github.com/consensys/gnark/internal/utils" -) - -func frontendVarToInt(a constraint.GkrVariable) int { - return int(a) -} - -func (api *API) NamedGate(gate string, in ...constraint.GkrVariable) constraint.GkrVariable { - api.toStore.Circuit = append(api.toStore.Circuit, constraint.GkrWire{ - Gate: gate, - Inputs: utils.Map(in, frontendVarToInt), - }) - api.assignments = append(api.assignments, nil) - return constraint.GkrVariable(len(api.toStore.Circuit) - 1) -} - -func (api *API) namedGate2PlusIn(gate string, in1, in2 constraint.GkrVariable, in ...constraint.GkrVariable) constraint.GkrVariable { - inCombined := make([]constraint.GkrVariable, 2+len(in)) - inCombined[0] = in1 - inCombined[1] = in2 - for i := range in { - inCombined[i+2] = in[i] - } - return api.NamedGate(gate, inCombined...) -} - -func (api *API) Add(i1, i2 constraint.GkrVariable, in ...constraint.GkrVariable) constraint.GkrVariable { - return api.namedGate2PlusIn("add", i1, i2, in...) -} - -func (api *API) Neg(i1 constraint.GkrVariable) constraint.GkrVariable { - return api.NamedGate("neg", i1) -} - -func (api *API) Sub(i1, i2 constraint.GkrVariable, in ...constraint.GkrVariable) constraint.GkrVariable { - return api.namedGate2PlusIn("sub", i1, i2, in...) -} - -func (api *API) Mul(i1, i2 constraint.GkrVariable, in ...constraint.GkrVariable) constraint.GkrVariable { - return api.namedGate2PlusIn("mul", i1, i2, in...) -} diff --git a/std/gkr/compile_test.go b/std/gkr/compile_test.go deleted file mode 100644 index 4e9affa9..00000000 --- a/std/gkr/compile_test.go +++ /dev/null @@ -1,132 +0,0 @@ -package gkr - -import ( - "github.com/consensys/gnark/constraint" - "github.com/stretchr/testify/assert" - "testing" -) - -func TestCompile2Cycles(t *testing.T) { - var d = constraint.GkrInfo{ - Circuit: constraint.GkrCircuit{ - { - Inputs: []int{1}, - Dependencies: nil, - }, - { - Inputs: []int{}, - Dependencies: []constraint.InputDependency{ - { - OutputWire: 0, - OutputInstance: 1, - InputInstance: 0, - }, - }, - }, - }, - } - - expectedCompiled := constraint.GkrInfo{ - Circuit: constraint.GkrCircuit{ - { - Inputs: []int{}, - Dependencies: []constraint.InputDependency{{ - OutputWire: 1, - OutputInstance: 0, - InputInstance: 1, - }}, - - NbUniqueOutputs: 1, - }, - { - Inputs: []int{0}, - Dependencies: nil, - }}, - MaxNIns: 1, - NbInstances: 2, - } - - expectedPermutations := constraint.GkrPermutations{ - SortedInstances: []int{1, 0}, - SortedWires: []int{1, 0}, - InstancesPermutation: []int{1, 0}, - WiresPermutation: []int{1, 0}, - } - - p, err := d.Compile(2) - assert.NoError(t, err) - assert.Equal(t, expectedPermutations, p) - assert.Equal(t, expectedCompiled, d) -} - -func TestCompile3Cycles(t *testing.T) { - var d = constraint.GkrInfo{ - Circuit: constraint.GkrCircuit{ - { - Inputs: []int{2}, - Dependencies: nil, - }, - { - Inputs: []int{}, - Dependencies: []constraint.InputDependency{ - { - OutputWire: 0, - OutputInstance: 2, - InputInstance: 0, - }, - { - OutputWire: 0, - OutputInstance: 1, - InputInstance: 2, - }, - }, - }, - { - Inputs: []int{1}, - Dependencies: nil, - }, - }, - } - - expectedCompiled := constraint.GkrInfo{ - Circuit: constraint.GkrCircuit{ - { - Inputs: []int{}, - Dependencies: []constraint.InputDependency{{ - OutputWire: 2, - OutputInstance: 0, - InputInstance: 1, - }, { - OutputWire: 2, - OutputInstance: 1, - InputInstance: 2, - }}, - NbUniqueOutputs: 1, - }, - { - Inputs: []int{0}, - Dependencies: nil, - NbUniqueOutputs: 1, - }, - { - Inputs: []int{1}, - Dependencies: nil, - NbUniqueOutputs: 0, - }, - }, - MaxNIns: 1, - NbInstances: 3, // not allowed if we were actually performing gkr - } - - expectedPermutations := constraint.GkrPermutations{ - SortedInstances: []int{1, 2, 0}, - SortedWires: []int{1, 2, 0}, - InstancesPermutation: []int{2, 0, 1}, - WiresPermutation: []int{2, 0, 1}, - } - - p, err := d.Compile(3) - assert.NoError(t, err) - assert.Equal(t, expectedPermutations, p) - assert.Equal(t, expectedCompiled, d) -} diff --git a/std/gkr/gkr.go b/std/gkr/gkr.go deleted file mode 100644 index 0da52730..00000000 --- a/std/gkr/gkr.go +++ /dev/null @@ -1,571 +0,0 @@ -package gkr - -import ( - "errors" - "fmt" - "strconv" - - "github.com/consensys/gnark/frontend" - fiatshamir "github.com/consensys/gnark/std/fiat-shamir" - "github.com/consensys/gnark/std/polynomial" - "github.com/consensys/gnark/std/sumcheck" -) - -// @tabaie TODO: Contains many things copy-pasted from gnark-crypto. Generify somehow? - -// The goal is to prove/verify evaluations of many instances of the same circuit - -// Gate must be a low-degree polynomial -type Gate interface { - Evaluate(frontend.API, ...frontend.Variable) frontend.Variable - Degree() int -} - -type Wire struct { - Gate Gate - Inputs []*Wire // if there are no Inputs, the wire is assumed an input wire - nbUniqueOutputs int // number of other wires using it as input, not counting duplicates (i.e. providing two inputs to the same gate counts as one) -} - -type Circuit []Wire - -func (w Wire) IsInput() bool { - return len(w.Inputs) == 0 -} - -func (w Wire) IsOutput() bool { - return w.nbUniqueOutputs == 0 -} - -func (w Wire) NbClaims() int { - if w.IsOutput() { - return 1 - } - return w.nbUniqueOutputs -} - -func (w Wire) nbUniqueInputs() int { - set := make(map[*Wire]struct{}, len(w.Inputs)) - for _, in := range w.Inputs { - set[in] = struct{}{} - } - return len(set) -} - -func (w Wire) noProof() bool { - return w.IsInput() && w.NbClaims() == 1 -} - -// WireAssignment is assignment of values to the same wire across many instances of the circuit -type WireAssignment map[*Wire]polynomial.MultiLin - -type Proof []sumcheck.Proof // for each layer, for each wire, a sumcheck (for each variable, a polynomial) - -type eqTimesGateEvalSumcheckLazyClaims struct { - wire *Wire - evaluationPoints [][]frontend.Variable - claimedEvaluations []frontend.Variable - manager *claimsManager // WARNING: Circular references -} - -func (e *eqTimesGateEvalSumcheckLazyClaims) VerifyFinalEval(api frontend.API, r []frontend.Variable, combinationCoeff, purportedValue frontend.Variable, proof interface{}) error { - inputEvaluationsNoRedundancy := proof.([]frontend.Variable) - - // the eq terms - numClaims := len(e.evaluationPoints) - evaluation := polynomial.EvalEq(api, e.evaluationPoints[numClaims-1], r) - for i := numClaims - 2; i >= 0; i-- { - evaluation = api.Mul(evaluation, combinationCoeff) - eq := polynomial.EvalEq(api, e.evaluationPoints[i], r) - evaluation = api.Add(evaluation, eq) - } - - // the g(...) term - var gateEvaluation frontend.Variable - if e.wire.IsInput() { - gateEvaluation = e.manager.assignment[e.wire].Evaluate(api, r) - } else { - inputEvaluations := make([]frontend.Variable, len(e.wire.Inputs)) - indexesInProof := make(map[*Wire]int, len(inputEvaluationsNoRedundancy)) - - proofI := 0 - for inI, in := range e.wire.Inputs { - indexInProof, found := indexesInProof[in] - if !found { - indexInProof = proofI - indexesInProof[in] = indexInProof - - // defer verification, store new claim - e.manager.add(in, r, inputEvaluationsNoRedundancy[indexInProof]) - proofI++ - } - inputEvaluations[inI] = inputEvaluationsNoRedundancy[indexInProof] - } - if proofI != len(inputEvaluationsNoRedundancy) { - return fmt.Errorf("%d input wire evaluations given, %d expected", len(inputEvaluationsNoRedundancy), proofI) - } - gateEvaluation = e.wire.Gate.Evaluate(api, inputEvaluations...) - } - evaluation = api.Mul(evaluation, gateEvaluation) - - api.AssertIsEqual(evaluation, purportedValue) - return nil -} - -func (e *eqTimesGateEvalSumcheckLazyClaims) ClaimsNum() int { - return len(e.evaluationPoints) -} - -func (e *eqTimesGateEvalSumcheckLazyClaims) VarsNum() int { - return len(e.evaluationPoints[0]) -} - -func (e *eqTimesGateEvalSumcheckLazyClaims) CombinedSum(api frontend.API, a frontend.Variable) frontend.Variable { - evalsAsPoly := polynomial.Polynomial(e.claimedEvaluations) - return evalsAsPoly.Eval(api, a) -} - -func (e *eqTimesGateEvalSumcheckLazyClaims) Degree(int) int { - return 1 + e.wire.Gate.Degree() -} - -type claimsManager struct { - claimsMap map[*Wire]*eqTimesGateEvalSumcheckLazyClaims - assignment WireAssignment -} - -func newClaimsManager(c Circuit, assignment WireAssignment) (claims claimsManager) { - claims.assignment = assignment - claims.claimsMap = make(map[*Wire]*eqTimesGateEvalSumcheckLazyClaims, len(c)) - - for i := range c { - wire := &c[i] - - claims.claimsMap[wire] = &eqTimesGateEvalSumcheckLazyClaims{ - wire: wire, - evaluationPoints: make([][]frontend.Variable, 0, wire.NbClaims()), - claimedEvaluations: make(polynomial.Polynomial, wire.NbClaims()), - manager: &claims, - } - } - return -} - -func (m *claimsManager) add(wire *Wire, evaluationPoint []frontend.Variable, evaluation frontend.Variable) { - claim := m.claimsMap[wire] - i := len(claim.evaluationPoints) - claim.claimedEvaluations[i] = evaluation - claim.evaluationPoints = append(claim.evaluationPoints, evaluationPoint) -} - -func (m *claimsManager) getLazyClaim(wire *Wire) *eqTimesGateEvalSumcheckLazyClaims { - return m.claimsMap[wire] -} - -func (m *claimsManager) deleteClaim(wire *Wire) { - delete(m.claimsMap, wire) -} - -type settings struct { - sorted []*Wire - transcript *fiatshamir.Transcript - transcriptPrefix string - nbVars int -} - -type Option func(*settings) - -func WithSortedCircuit(sorted []*Wire) Option { - return func(options *settings) { - options.sorted = sorted - } -} - -func setup(api frontend.API, c Circuit, assignment WireAssignment, transcriptSettings fiatshamir.Settings, options ...Option) (settings, error) { - var o settings - var err error - for _, option := range options { - option(&o) - } - - o.nbVars = assignment.NumVars() - nbInstances := assignment.NumInstances() - if 1< b { - return a - } - return b -} - -func ChallengeNames(sorted []*Wire, logNbInstances int, prefix string) []string { - - // Pre-compute the size TODO: Consider not doing this and just grow the list by appending - size := logNbInstances // first challenge - - for _, w := range sorted { - if w.noProof() { // no proof, no challenge - continue - } - if w.NbClaims() > 1 { //combine the claims - size++ - } - size += logNbInstances // full run of sumcheck on logNbInstances variables - } - - nums := make([]string, max(len(sorted), logNbInstances)) - for i := range nums { - nums[i] = strconv.Itoa(i) - } - - challenges := make([]string, size) - - // output wire claims - firstChallengePrefix := prefix + "fC." - for j := 0; j < logNbInstances; j++ { - challenges[j] = firstChallengePrefix + nums[j] - } - j := logNbInstances - for i := len(sorted) - 1; i >= 0; i-- { - if sorted[i].noProof() { - continue - } - wirePrefix := prefix + "w" + nums[i] + "." - - if sorted[i].NbClaims() > 1 { - challenges[j] = wirePrefix + "comb" - j++ - } - - partialSumPrefix := wirePrefix + "pSP." - for k := 0; k < logNbInstances; k++ { - challenges[j] = partialSumPrefix + nums[k] - j++ - } - } - return challenges -} - -func getFirstChallengeNames(logNbInstances int, prefix string) []string { - res := make([]string, logNbInstances) - firstChallengePrefix := prefix + "fC." - for i := 0; i < logNbInstances; i++ { - res[i] = firstChallengePrefix + strconv.Itoa(i) - } - return res -} - -func getChallenges(transcript *fiatshamir.Transcript, names []string) (challenges []frontend.Variable, err error) { - challenges = make([]frontend.Variable, len(names)) - for i, name := range names { - if challenges[i], err = transcript.ComputeChallenge(name); err != nil { - return - } - } - return -} - -// Verify the consistency of the claimed output with the claimed input -// Unlike in Prove, the assignment argument need not be complete -func Verify(api frontend.API, c Circuit, assignment WireAssignment, proof Proof, transcriptSettings fiatshamir.Settings, options ...Option) error { - o, err := setup(api, c, assignment, transcriptSettings, options...) - if err != nil { - return err - } - - claims := newClaimsManager(c, assignment) - - var firstChallenge []frontend.Variable - firstChallenge, err = getChallenges(o.transcript, getFirstChallengeNames(o.nbVars, o.transcriptPrefix)) - if err != nil { - return err - } - - wirePrefix := o.transcriptPrefix + "w" - var baseChallenge []frontend.Variable - for i := len(c) - 1; i >= 0; i-- { - wire := o.sorted[i] - - if wire.IsOutput() { - claims.add(wire, firstChallenge, assignment[wire].Evaluate(api, firstChallenge)) - } - - proofW := proof[i] - finalEvalProof := proofW.FinalEvalProof.([]frontend.Variable) - claim := claims.getLazyClaim(wire) - if wire.noProof() { // input wires with one claim only - // make sure the proof is empty - if len(finalEvalProof) != 0 || len(proofW.PartialSumPolys) != 0 { - return errors.New("no proof allowed for input wire with a single claim") - } - - if wire.NbClaims() == 1 { // input wire - // simply evaluate and see if it matches - evaluation := assignment[wire].Evaluate(api, claim.evaluationPoints[0]) - api.AssertIsEqual(claim.claimedEvaluations[0], evaluation) - } - } else if err = sumcheck.Verify( - api, claim, proof[i], fiatshamir.WithTranscript(o.transcript, wirePrefix+strconv.Itoa(i)+".", baseChallenge...), - ); err == nil { - baseChallenge = finalEvalProof - } else { - return err - } - claims.deleteClaim(wire) - } - return nil -} - -type IdentityGate struct{} - -func (IdentityGate) Evaluate(_ frontend.API, input ...frontend.Variable) frontend.Variable { - return input[0] -} - -func (IdentityGate) Degree() int { - return 1 -} - -// outputsList also sets the nbUniqueOutputs fields. It also sets the wire metadata. -func outputsList(c Circuit, indexes map[*Wire]int) [][]int { - res := make([][]int, len(c)) - for i := range c { - res[i] = make([]int, 0) - c[i].nbUniqueOutputs = 0 - if c[i].IsInput() { - c[i].Gate = IdentityGate{} - } - } - ins := make(map[int]struct{}, len(c)) - for i := range c { - for k := range ins { // clear map - delete(ins, k) - } - for _, in := range c[i].Inputs { - inI := indexes[in] - res[inI] = append(res[inI], i) - if _, ok := ins[inI]; !ok { - in.nbUniqueOutputs++ - ins[inI] = struct{}{} - } - } - } - return res -} - -type topSortData struct { - outputs [][]int - status []int // status > 0 indicates number of inputs left to be ready. status = 0 means ready. status = -1 means done - index map[*Wire]int - leastReady int -} - -func (d *topSortData) markDone(i int) { - - d.status[i] = -1 - - for _, outI := range d.outputs[i] { - d.status[outI]-- - if d.status[outI] == 0 && outI < d.leastReady { - d.leastReady = outI - } - } - - for d.leastReady < len(d.status) && d.status[d.leastReady] != 0 { - d.leastReady++ - } -} - -func indexMap(c Circuit) map[*Wire]int { - res := make(map[*Wire]int, len(c)) - for i := range c { - res[&c[i]] = i - } - return res -} - -func statusList(c Circuit) []int { - res := make([]int, len(c)) - for i := range c { - res[i] = len(c[i].Inputs) - } - return res -} - -// TODO: Have this use algo_utils.TopologicalSort underneath - -// topologicalSort sorts the wires in order of dependence. Such that for any wire, any one it depends on -// occurs before it. It tries to stick to the input order as much as possible. An already sorted list will remain unchanged. -// It also sets the nbOutput flags, and a dummy IdentityGate for input wires. -// Worst-case inefficient O(n^2), but that probably won't matter since the circuits are small. -// Furthermore, it is efficient with already-close-to-sorted lists, which are the expected input -func topologicalSort(c Circuit) []*Wire { - var data topSortData - data.index = indexMap(c) - data.outputs = outputsList(c, data.index) - data.status = statusList(c) - sorted := make([]*Wire, len(c)) - - for data.leastReady = 0; data.status[data.leastReady] != 0; data.leastReady++ { - } - - for i := range c { - sorted[i] = &c[data.leastReady] - data.markDone(data.leastReady) - } - - return sorted -} - -func (a WireAssignment) NumInstances() int { - for _, aW := range a { - if aW != nil { - return len(aW) - } - } - panic("empty assignment") -} - -func (a WireAssignment) NumVars() int { - for _, aW := range a { - if aW != nil { - return aW.NumVars() - } - } - panic("empty assignment") -} - -func (p Proof) Serialize() []frontend.Variable { - size := 0 - for i := range p { - for j := range p[i].PartialSumPolys { - size += len(p[i].PartialSumPolys[j]) - } - size += len(p[i].FinalEvalProof.([]frontend.Variable)) - } - - res := make([]frontend.Variable, 0, size) - for i := range p { - for j := range p[i].PartialSumPolys { - res = append(res, p[i].PartialSumPolys[j]...) - } - res = append(res, p[i].FinalEvalProof.([]frontend.Variable)...) - } - if len(res) != size { - panic("bug") // TODO: Remove - } - return res -} - -func computeLogNbInstances(wires []*Wire, serializedProofLen int) int { - partialEvalElemsPerVar := 0 - for _, w := range wires { - if !w.noProof() { - partialEvalElemsPerVar += w.Gate.Degree() + 1 - } - serializedProofLen -= w.nbUniqueOutputs - } - return serializedProofLen / partialEvalElemsPerVar -} - -type variablesReader []frontend.Variable - -func (r *variablesReader) nextN(n int) []frontend.Variable { - res := (*r)[:n] - *r = (*r)[n:] - return res -} - -func (r *variablesReader) hasNextN(n int) bool { - return len(*r) >= n -} - -func DeserializeProof(sorted []*Wire, serializedProof []frontend.Variable) (Proof, error) { - proof := make(Proof, len(sorted)) - logNbInstances := computeLogNbInstances(sorted, len(serializedProof)) - - reader := variablesReader(serializedProof) - for i, wI := range sorted { - if !wI.noProof() { - proof[i].PartialSumPolys = make([]polynomial.Polynomial, logNbInstances) - for j := range proof[i].PartialSumPolys { - proof[i].PartialSumPolys[j] = reader.nextN(wI.Gate.Degree() + 1) - } - } - proof[i].FinalEvalProof = reader.nextN(wI.nbUniqueInputs()) - } - if reader.hasNextN(1) { - return nil, fmt.Errorf("proof too long: expected %d encountered %d", len(serializedProof)-len(reader), len(serializedProof)) - } - return proof, nil -} - -type MulGate struct{} - -func (g MulGate) Evaluate(api frontend.API, x ...frontend.Variable) frontend.Variable { - if len(x) != 2 { - panic("mul has fan-in 2") - } - return api.Mul(x[0], x[1]) -} - -// TODO: Degree must take nbInputs as an argument and return degree = nbInputs -func (g MulGate) Degree() int { - return 2 -} - -type AddGate struct{} - -func (a AddGate) Evaluate(api frontend.API, v ...frontend.Variable) frontend.Variable { - switch len(v) { - case 0: - return 0 - case 1: - return v[0] - } - rest := v[2:] - return api.Add(v[0], v[1], rest...) -} - -func (a AddGate) Degree() int { - return 1 -} - -var Gates = map[string]Gate{ - "identity": IdentityGate{}, - "add": AddGate{}, - "mul": MulGate{}, -} diff --git a/std/gkr/hints.go b/std/gkr/hints.go deleted file mode 100644 index 8cfdaa64..00000000 --- a/std/gkr/hints.go +++ /dev/null @@ -1,102 +0,0 @@ -package gkr - -import ( - "errors" - "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark/constraint" - bls12377 "github.com/consensys/gnark/constraint/bls12-377" - bls12381 "github.com/consensys/gnark/constraint/bls12-381" - bls24315 "github.com/consensys/gnark/constraint/bls24-315" - bls24317 "github.com/consensys/gnark/constraint/bls24-317" - bn254 "github.com/consensys/gnark/constraint/bn254" - bw6633 "github.com/consensys/gnark/constraint/bw6-633" - bw6761 "github.com/consensys/gnark/constraint/bw6-761" - "github.com/consensys/gnark/constraint/solver" - "math/big" -) - -var testEngineGkrSolvingData = make(map[string]any) - -func modKey(mod *big.Int) string { - return mod.Text(32) -} - -func SolveHintPlaceholder(gkrInfo constraint.GkrInfo) solver.Hint { - return func(mod *big.Int, ins []*big.Int, outs []*big.Int) error { - - // TODO @Tabaie autogenerate this or decide not to - if mod.Cmp(ecc.BLS12_377.ScalarField()) == 0 { - var data bls12377.GkrSolvingData - testEngineGkrSolvingData[modKey(mod)] = &data - return bls12377.GkrSolveHint(gkrInfo, &data)(mod, ins, outs) - } - if mod.Cmp(ecc.BLS12_381.ScalarField()) == 0 { - var data bls12381.GkrSolvingData - testEngineGkrSolvingData[modKey(mod)] = &data - return bls12381.GkrSolveHint(gkrInfo, &data)(mod, ins, outs) - } - if mod.Cmp(ecc.BLS24_315.ScalarField()) == 0 { - var data bls24315.GkrSolvingData - testEngineGkrSolvingData[modKey(mod)] = &data - return bls24315.GkrSolveHint(gkrInfo, &data)(mod, ins, outs) - } - if mod.Cmp(ecc.BLS24_317.ScalarField()) == 0 { - var data bls24317.GkrSolvingData - testEngineGkrSolvingData[modKey(mod)] = &data - return bls24317.GkrSolveHint(gkrInfo, &data)(mod, ins, outs) - } - if mod.Cmp(ecc.BN254.ScalarField()) == 0 { - var data bn254.GkrSolvingData - testEngineGkrSolvingData[modKey(mod)] = &data - return bn254.GkrSolveHint(gkrInfo, &data)(mod, ins, outs) - } - if mod.Cmp(ecc.BW6_633.ScalarField()) == 0 { - var data bw6633.GkrSolvingData - testEngineGkrSolvingData[modKey(mod)] = &data - return bw6633.GkrSolveHint(gkrInfo, &data)(mod, ins, outs) - } - if mod.Cmp(ecc.BW6_761.ScalarField()) == 0 { - var data bw6761.GkrSolvingData - testEngineGkrSolvingData[modKey(mod)] = &data - return bw6761.GkrSolveHint(gkrInfo, &data)(mod, ins, outs) - } - - return errors.New("unsupported modulus") - } -} - -func ProveHintPlaceholder(hashName string) solver.Hint { - return func(mod *big.Int, ins, outs []*big.Int) error { - k := modKey(mod) - data, ok := testEngineGkrSolvingData[k] - if !ok { - return errors.New("solving data not found") - } - delete(testEngineGkrSolvingData, k) - - // TODO @Tabaie autogenerate this or decide not to - if mod.Cmp(ecc.BLS12_377.ScalarField()) == 0 { - return bls12377.GkrProveHint(hashName, data.(*bls12377.GkrSolvingData))(mod, ins, outs) - } - if mod.Cmp(ecc.BLS12_381.ScalarField()) == 0 { - return bls12381.GkrProveHint(hashName, data.(*bls12381.GkrSolvingData))(mod, ins, outs) - } - if mod.Cmp(ecc.BLS24_315.ScalarField()) == 0 { - return bls24315.GkrProveHint(hashName, data.(*bls24315.GkrSolvingData))(mod, ins, outs) - } - if mod.Cmp(ecc.BLS24_317.ScalarField()) == 0 { - return bls24317.GkrProveHint(hashName, data.(*bls24317.GkrSolvingData))(mod, ins, outs) - } - if mod.Cmp(ecc.BN254.ScalarField()) == 0 { - return bn254.GkrProveHint(hashName, data.(*bn254.GkrSolvingData))(mod, ins, outs) - } - if mod.Cmp(ecc.BW6_633.ScalarField()) == 0 { - return bw6633.GkrProveHint(hashName, data.(*bw6633.GkrSolvingData))(mod, ins, outs) - } - if mod.Cmp(ecc.BW6_761.ScalarField()) == 0 { - return bw6761.GkrProveHint(hashName, data.(*bw6761.GkrSolvingData))(mod, ins, outs) - } - - return errors.New("unsupported modulus") - } -} diff --git a/std/gkr/testing.go b/std/gkr/testing.go deleted file mode 100644 index 50111a60..00000000 --- a/std/gkr/testing.go +++ /dev/null @@ -1,234 +0,0 @@ -package gkr - -import ( - "errors" - "fmt" - "math/big" - - "github.com/consensys/gnark-crypto/ecc" - frBls12377 "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" - gkrBls12377 "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/gkr" - frBls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381/fr" - gkrBls12381 "github.com/consensys/gnark-crypto/ecc/bls12-381/fr/gkr" - frBls24315 "github.com/consensys/gnark-crypto/ecc/bls24-315/fr" - gkrBls24315 "github.com/consensys/gnark-crypto/ecc/bls24-315/fr/gkr" - frBls24317 "github.com/consensys/gnark-crypto/ecc/bls24-317/fr" - gkrBls24317 "github.com/consensys/gnark-crypto/ecc/bls24-317/fr/gkr" - frBn254 "github.com/consensys/gnark-crypto/ecc/bn254/fr" - gkrBn254 "github.com/consensys/gnark-crypto/ecc/bn254/fr/gkr" - frBw6633 "github.com/consensys/gnark-crypto/ecc/bw6-633/fr" - gkrBw6633 "github.com/consensys/gnark-crypto/ecc/bw6-633/fr/gkr" - frBw6761 "github.com/consensys/gnark-crypto/ecc/bw6-761/fr" - gkrBw6761 "github.com/consensys/gnark-crypto/ecc/bw6-761/fr/gkr" - hint "github.com/consensys/gnark/constraint/solver" - "github.com/consensys/gnark/frontend" -) - -// SolveInTestEngine solves the defined circuit directly inside the SNARK circuit. This means that the method does not compute the GKR proof of the circuit and does not embed the GKR proof verifier inside a SNARK. -// The output is the values of all variables, across all instances; i.e. indexed variable-first, instance-second. -// This method only works under the test engine and should only be called to debug a GKR circuit, as the GKR prover's errors can be obscure. -func (api *API) SolveInTestEngine(parentApi frontend.API) [][]frontend.Variable { - res := make([][]frontend.Variable, len(api.toStore.Circuit)) - degreeTestedGates := make(map[string]struct{}) - for i, w := range api.toStore.Circuit { - res[i] = make([]frontend.Variable, api.nbInstances()) - copy(res[i], api.assignments[i]) - if len(w.Inputs) == 0 { - continue - } - degree := Gates[w.Gate].Degree() - var degreeFr int - if parentApi.Compiler().Field().Cmp(ecc.BLS12_377.ScalarField()) == 0 { - degreeFr = gkrBls12377.Gates[w.Gate].Degree() - } else if parentApi.Compiler().Field().Cmp(ecc.BN254.ScalarField()) == 0 { - degreeFr = gkrBn254.Gates[w.Gate].Degree() - } else if parentApi.Compiler().Field().Cmp(ecc.BLS24_315.ScalarField()) == 0 { - degreeFr = gkrBls24315.Gates[w.Gate].Degree() - } else if parentApi.Compiler().Field().Cmp(ecc.BW6_761.ScalarField()) == 0 { - degreeFr = gkrBw6761.Gates[w.Gate].Degree() - } else if parentApi.Compiler().Field().Cmp(ecc.BLS12_381.ScalarField()) == 0 { - degreeFr = gkrBls12381.Gates[w.Gate].Degree() - } else if parentApi.Compiler().Field().Cmp(ecc.BLS24_317.ScalarField()) == 0 { - degreeFr = gkrBls24317.Gates[w.Gate].Degree() - } else if parentApi.Compiler().Field().Cmp(ecc.BW6_633.ScalarField()) == 0 { - degreeFr = gkrBw6633.Gates[w.Gate].Degree() - } else { - panic("field not yet supported") - } - if degree != degreeFr { - panic(fmt.Errorf("gate \"%s\" degree mismatch: SNARK %d, Raw %d", w.Gate, degree, degreeFr)) - } - } - for instanceI := range api.nbInstances() { - for wireI, w := range api.toStore.Circuit { - if len(w.Dependencies) != 0 && len(w.Inputs) != 0 { - panic(fmt.Errorf("non-input wire %d should not have dependencies", wireI)) - } - for _, dep := range w.Dependencies { - if dep.InputInstance == instanceI { - if dep.OutputInstance >= instanceI { - panic(fmt.Errorf("out of order dependency not yet supported in SolveInTestEngine; (wire %d, instance %d) depends on (wire %d, instance %d)", wireI, instanceI, dep.OutputWire, dep.OutputInstance)) - } - if res[wireI][instanceI] != nil { - panic(fmt.Errorf("dependency (wire %d, instance %d) <- (wire %d, instance %d) attempting to override existing value assignment", wireI, instanceI, dep.OutputWire, dep.OutputInstance)) - } - res[wireI][instanceI] = res[dep.OutputWire][dep.OutputInstance] - } - } - - if res[wireI][instanceI] == nil { // no assignment or dependency - if len(w.Inputs) == 0 { - panic(fmt.Errorf("input wire %d, instance %d has no dependency or explicit assignment", wireI, instanceI)) - } - ins := make([]frontend.Variable, len(w.Inputs)) - for i, in := range w.Inputs { - ins[i] = res[in][instanceI] - } - expectedV, err := parentApi.Compiler().NewHint(frGateHint(w.Gate, degreeTestedGates), 1, ins...) - if err != nil { - panic(err) - } - res[wireI][instanceI] = Gates[w.Gate].Evaluate(parentApi, ins...) - parentApi.AssertIsEqual(expectedV[0], res[wireI][instanceI]) // snark and raw gate evaluations must agree - } - } - } - return res -} - -func frGateHint(gateName string, degreeTestedGates map[string]struct{}) hint.Hint { - return func(mod *big.Int, ins, outs []*big.Int) error { - if len(outs) != 1 { - return errors.New("gate must have one output") - } - if ecc.BLS12_377.ScalarField().Cmp(mod) == 0 { - gate := gkrBls12377.Gates[gateName] - if gate == nil { - return fmt.Errorf("gate \"%s\" not found", gateName) - } - if _, ok := degreeTestedGates[gateName]; !ok { - if err := gkrBls12377.TestGateDegree(gate, len(ins)); err != nil { - return fmt.Errorf("gate %s: %w", gateName, err) - } - degreeTestedGates[gateName] = struct{}{} - } - - x := make([]frBls12377.Element, len(ins)) - for i := range ins { - x[i].SetBigInt(ins[i]) - } - y := gate.Evaluate(x...) - y.BigInt(outs[0]) - } else if ecc.BN254.ScalarField().Cmp(mod) == 0 { - gate := gkrBn254.Gates[gateName] - if gate == nil { - return fmt.Errorf("gate \"%s\" not found", gateName) - } - if _, ok := degreeTestedGates[gateName]; !ok { - if err := gkrBn254.TestGateDegree(gate, len(ins)); err != nil { - return fmt.Errorf("gate %s: %w", gateName, err) - } - degreeTestedGates[gateName] = struct{}{} - } - - x := make([]frBn254.Element, len(ins)) - for i := range ins { - x[i].SetBigInt(ins[i]) - } - y := gate.Evaluate(x...) - y.BigInt(outs[0]) - } else if ecc.BLS24_315.ScalarField().Cmp(mod) == 0 { - gate := gkrBls24315.Gates[gateName] - if gate == nil { - return fmt.Errorf("gate \"%s\" not found", gateName) - } - if _, ok := degreeTestedGates[gateName]; !ok { - if err := gkrBls24315.TestGateDegree(gate, len(ins)); err != nil { - return fmt.Errorf("gate %s: %w", gateName, err) - } - degreeTestedGates[gateName] = struct{}{} - } - - x := make([]frBls24315.Element, len(ins)) - for i := range ins { - x[i].SetBigInt(ins[i]) - } - y := gate.Evaluate(x...) - y.BigInt(outs[0]) - } else if ecc.BW6_761.ScalarField().Cmp(mod) == 0 { - gate := gkrBw6761.Gates[gateName] - if gate == nil { - return fmt.Errorf("gate \"%s\" not found", gateName) - } - if _, ok := degreeTestedGates[gateName]; !ok { - if err := gkrBw6761.TestGateDegree(gate, len(ins)); err != nil { - return fmt.Errorf("gate %s: %w", gateName, err) - } - degreeTestedGates[gateName] = struct{}{} - } - - x := make([]frBw6761.Element, len(ins)) - for i := range ins { - x[i].SetBigInt(ins[i]) - } - y := gate.Evaluate(x...) - y.BigInt(outs[0]) - } else if ecc.BLS12_381.ScalarField().Cmp(mod) == 0 { - gate := gkrBls12381.Gates[gateName] - if gate == nil { - return fmt.Errorf("gate \"%s\" not found", gateName) - } - if _, ok := degreeTestedGates[gateName]; !ok { - if err := gkrBls12381.TestGateDegree(gate, len(ins)); err != nil { - return fmt.Errorf("gate %s: %w", gateName, err) - } - degreeTestedGates[gateName] = struct{}{} - } - - x := make([]frBls12381.Element, len(ins)) - for i := range ins { - x[i].SetBigInt(ins[i]) - } - y := gate.Evaluate(x...) - y.BigInt(outs[0]) - } else if ecc.BLS24_317.ScalarField().Cmp(mod) == 0 { - gate := gkrBls24317.Gates[gateName] - if gate == nil { - return fmt.Errorf("gate \"%s\" not found", gateName) - } - if _, ok := degreeTestedGates[gateName]; !ok { - if err := gkrBls24317.TestGateDegree(gate, len(ins)); err != nil { - return fmt.Errorf("gate %s: %w", gateName, err) - } - degreeTestedGates[gateName] = struct{}{} - } - - x := make([]frBls24317.Element, len(ins)) - for i := range ins { - x[i].SetBigInt(ins[i]) - } - y := gate.Evaluate(x...) - y.BigInt(outs[0]) - } else if ecc.BW6_633.ScalarField().Cmp(mod) == 0 { - gate := gkrBw6633.Gates[gateName] - if gate == nil { - return fmt.Errorf("gate \"%s\" not found", gateName) - } - if _, ok := degreeTestedGates[gateName]; !ok { - if err := gkrBw6633.TestGateDegree(gate, len(ins)); err != nil { - return fmt.Errorf("gate %s: %w", gateName, err) - } - degreeTestedGates[gateName] = struct{}{} - } - x := make([]frBw6633.Element, len(ins)) - for i := range ins { - x[i].SetBigInt(ins[i]) - } - y := gate.Evaluate(x...) - y.BigInt(outs[0]) - } else { - return errors.New("field not supported") - } - return nil - } -} diff --git a/std/gkrapi/api.go b/std/gkrapi/api.go new file mode 100644 index 00000000..771613ce --- /dev/null +++ b/std/gkrapi/api.go @@ -0,0 +1,83 @@ +package gkrapi + +import ( + "github.com/consensys/gnark/constraint/solver/gkrgates" + "github.com/consensys/gnark/internal/gkr/gkrinfo" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/internal/utils" + "github.com/consensys/gnark/std/gkrapi/gkr" +) + +type API struct { + toStore gkrinfo.StoringInfo + assignments gkrtypes.WireAssignment +} + +func frontendVarToInt(a gkr.Variable) int { + return int(a) +} + +func (api *API) NamedGate(gate gkr.GateName, in ...gkr.Variable) gkr.Variable { + api.toStore.Circuit = append(api.toStore.Circuit, gkrinfo.Wire{ + Gate: string(gate), + Inputs: utils.Map(in, frontendVarToInt), + }) + api.assignments = append(api.assignments, nil) + api.toStore.Dependencies = append(api.toStore.Dependencies, nil) // formality. Dependencies are only defined for input vars. + return gkr.Variable(len(api.toStore.Circuit) - 1) +} + +func (api *API) Gate(gate gkr.GateFunction, in ...gkr.Variable) gkr.Variable { + if err := gkrgates.Register(gate, len(in)); err != nil { + panic(err) + } + return api.NamedGate(gkrgates.GetDefaultGateName(gate), in...) +} + +func (api *API) namedGate2PlusIn(gate gkr.GateName, in1, in2 gkr.Variable, in ...gkr.Variable) gkr.Variable { + inCombined := make([]gkr.Variable, 2+len(in)) + inCombined[0] = in1 + inCombined[1] = in2 + for i := range in { + inCombined[i+2] = in[i] + } + return api.NamedGate(gate, inCombined...) +} + +func (api *API) Add(i1, i2 gkr.Variable) gkr.Variable { + return api.namedGate2PlusIn(gkr.Add2, i1, i2) +} + +func (api *API) Neg(i1 gkr.Variable) gkr.Variable { + return api.NamedGate("neg", i1) +} + +func (api *API) Sub(i1, i2 gkr.Variable) gkr.Variable { + return api.namedGate2PlusIn(gkr.Sub2, i1, i2) +} + +func (api *API) Mul(i1, i2 gkr.Variable) gkr.Variable { + return api.namedGate2PlusIn(gkr.Mul2, i1, i2) +} + +// Println writes to the standard output. +// instance determines which values are chosen for gkr.Variable input. +func (api *API) Println(instance int, a ...any) { + isVar := make([]bool, len(a)) + vals := make([]any, len(a)) + for i := range a { + v, ok := a[i].(gkr.Variable) + isVar[i] = ok + if ok { + vals[i] = uint32(v) + } else { + vals[i] = a[i] + } + } + + api.toStore.Prints = append(api.toStore.Prints, gkrinfo.PrintInfo{ + Values: vals, + Instance: uint32(instance), + IsGkrVar: isVar, + }) +} diff --git a/std/gkr/api_test.go b/std/gkrapi/api_test.go similarity index 67% rename from std/gkr/api_test.go rename to std/gkrapi/api_test.go index 10817fed..5823c687 100644 --- a/std/gkr/api_test.go +++ b/std/gkrapi/api_test.go @@ -1,35 +1,30 @@ -package gkr +package gkrapi import ( + "bytes" "fmt" "hash" + "math/big" "math/rand" + "slices" "strconv" + "strings" "testing" "time" - bls12377 "github.com/consensys/gnark/constraint/bls12-377" - bls12381 "github.com/consensys/gnark/constraint/bls12-381" - bls24315 "github.com/consensys/gnark/constraint/bls24-315" - bls24317 "github.com/consensys/gnark/constraint/bls24-317" - bw6633 "github.com/consensys/gnark/constraint/bw6-633" - bw6761 "github.com/consensys/gnark/constraint/bw6-761" - "github.com/consensys/gnark/test" - - bn254 "github.com/consensys/gnark/constraint/bn254" - "github.com/stretchr/testify/require" - + "github.com/consensys/gnark" "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark-crypto/ecc/bn254/fr" - "github.com/consensys/gnark-crypto/ecc/bn254/fr/gkr" - bn254MiMC "github.com/consensys/gnark-crypto/ecc/bn254/fr/mimc" + gcHash "github.com/consensys/gnark-crypto/hash" "github.com/consensys/gnark/backend/groth16" - "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/constraint/solver/gkrgates" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/internal/gkr/gkrinfo" + "github.com/consensys/gnark/std/gkrapi/gkr" stdHash "github.com/consensys/gnark/std/hash" - "github.com/consensys/gnark/std/hash/mimc" - test_vector_utils "github.com/consensys/gnark/std/internal/test_vectors_utils" + "github.com/consensys/gnark/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // compressThreshold --> if linear expressions are larger than this, the frontend will introduce @@ -43,15 +38,15 @@ type doubleNoDependencyCircuit struct { } func (c *doubleNoDependencyCircuit) Define(api frontend.API) error { - gkr := NewApi() - var x constraint.GkrVariable + gkrApi := New() + var x gkr.Variable var err error - if x, err = gkr.Import(c.X); err != nil { + if x, err = gkrApi.Import(c.X); err != nil { return err } - z := gkr.Add(x, x) + z := gkrApi.Add(x, x) var solution Solution - if solution, err = gkr.Solve(api); err != nil { + if solution, err = gkrApi.Solve(api); err != nil { return err } Z := solution.Export(z) @@ -91,15 +86,15 @@ type sqNoDependencyCircuit struct { } func (c *sqNoDependencyCircuit) Define(api frontend.API) error { - gkr := NewApi() - var x constraint.GkrVariable + gkrApi := New() + var x gkr.Variable var err error - if x, err = gkr.Import(c.X); err != nil { + if x, err = gkrApi.Import(c.X); err != nil { return err } - z := gkr.Mul(x, x) + z := gkrApi.Mul(x, x) var solution Solution - if solution, err = gkr.Solve(api); err != nil { + if solution, err = gkrApi.Solve(api); err != nil { return err } Z := solution.Export(z) @@ -138,28 +133,26 @@ type mulNoDependencyCircuit struct { } func (c *mulNoDependencyCircuit) Define(api frontend.API) error { - gkr := NewApi() - var x, y constraint.GkrVariable + gkrApi := New() + var x, y gkr.Variable var err error - if x, err = gkr.Import(c.X); err != nil { + if x, err = gkrApi.Import(c.X); err != nil { return err } - if y, err = gkr.Import(c.Y); err != nil { + if y, err = gkrApi.Import(c.Y); err != nil { return err } - z := gkr.Mul(x, y) + gkrApi.Println(0, "values of x and y in instance number", 0, x, y) + + z := gkrApi.Mul(x, y) + gkrApi.Println(1, "value of z in instance number", 1, z) var solution Solution - if solution, err = gkr.Solve(api); err != nil { + if solution, err = gkrApi.Solve(api); err != nil { return err } - X := solution.Export(x) - Y := solution.Export(y) Z := solution.Export(z) - api.Println("after solving, z=", Z, ", x=", X, ", y=", Y) for i := range c.X { - api.Println("z@", i, " = ", Z[i]) - api.Println("x.y = ", api.Mul(c.X[i], c.Y[i])) api.AssertIsEqual(Z[i], api.Mul(c.X[i], c.Y[i])) } @@ -203,34 +196,33 @@ type mulWithDependencyCircuit struct { } func (c *mulWithDependencyCircuit) Define(api frontend.API) error { - gkr := NewApi() - var x, y constraint.GkrVariable + gkrApi := New() + var x, y gkr.Variable var err error X := make([]frontend.Variable, len(c.Y)) X[len(c.Y)-1] = c.XLast - if x, err = gkr.Import(X); err != nil { + if x, err = gkrApi.Import(X); err != nil { return err } - if y, err = gkr.Import(c.Y); err != nil { + if y, err = gkrApi.Import(c.Y); err != nil { return err } - z := gkr.Mul(x, y) + + z := gkrApi.Mul(x, y) for i := len(X) - 1; i > 0; i-- { - gkr.Series(x, z, i-1, i) + gkrApi.Series(x, z, i-1, i) } var solution Solution - if solution, err = gkr.Solve(api); err != nil { + if solution, err = gkrApi.Solve(api); err != nil { return err } X = solution.Export(x) Y := solution.Export(y) Z := solution.Export(z) - api.Println("after solving, z=", Z, ", x=", X, ", y=", Y) - lastI := len(X) - 1 api.AssertIsEqual(Z[lastI], api.Mul(c.XLast, Y[lastI])) for i := 0; i < lastI; i++ { @@ -251,18 +243,18 @@ func TestSolveMulWithDependency(t *testing.T) { func TestApiMul(t *testing.T) { var ( - x constraint.GkrVariable - y constraint.GkrVariable - z constraint.GkrVariable + x gkr.Variable + y gkr.Variable + z gkr.Variable err error ) - api := NewApi() + api := New() x, err = api.Import([]frontend.Variable{nil, nil}) require.NoError(t, err) y, err = api.Import([]frontend.Variable{nil, nil}) require.NoError(t, err) z = api.Mul(x, y) - test_vector_utils.AssertSliceEqual(t, api.toStore.Circuit[z].Inputs, []int{int(x), int(y)}) // TODO: Find out why assert.Equal gives false positives ( []*Wire{x,x} as second argument passes when it shouldn't ) + assertSliceEqual(t, api.toStore.Circuit[z].Inputs, []int{int(x), int(y)}) // TODO: Find out why assert.Equal gives false positives ( []*Wire{x,x} as second argument passes when it shouldn't ) } func BenchmarkMiMCMerkleTree(b *testing.B) { @@ -342,36 +334,36 @@ func (c *benchMiMCMerkleTreeCircuit) Define(api frontend.API) error { X[len(X)-1] = 0 Y[len(X)-1] = 0 - var x, y constraint.GkrVariable + var x, y gkr.Variable var err error - gkr := NewApi() - if x, err = gkr.Import(X); err != nil { + gkrApi := New() + if x, err = gkrApi.Import(X); err != nil { return err } - if y, err = gkr.Import(Y); err != nil { + if y, err = gkrApi.Import(Y); err != nil { return err } // cheat{ - gkr.toStore.Circuit = append(gkr.toStore.Circuit, constraint.GkrWire{ - Gate: "mimc", + gkrApi.toStore.Circuit = append(gkrApi.toStore.Circuit, gkrinfo.Wire{ + Gate: "MIMC", Inputs: []int{int(x), int(y)}, }) - gkr.assignments = append(gkr.assignments, nil) - z := constraint.GkrVariable(2) + gkrApi.assignments = append(gkrApi.assignments, nil) + z := gkr.Variable(2) // } offset := 1 << (c.depth - 1) for d := c.depth - 2; d >= 0; d-- { for i := 0; i < 1< dst = [0,1,2,3]. func appendNonNil(dst *[]frontend.Variable, src []frontend.Variable) { for i := range src { if src[i] != nil { @@ -93,7 +92,7 @@ func appendNonNil(dst *[]frontend.Variable, src []frontend.Variable) { // Solve finalizes the GKR circuit and returns the output variables in the order created func (api *API) Solve(parentApi frontend.API) (Solution, error) { - var p constraint.GkrPermutations + var p gkrinfo.Permutations var err error if p, err = api.toStore.Compile(api.assignments.NbInstances()); err != nil { return Solution{}, err @@ -108,9 +107,14 @@ func (api *API) Solve(parentApi frontend.API) (Solution, error) { for i := range circuit { v := &circuit[i] - if v.IsInput() { - solveHintNIn += nbInstances - len(v.Dependencies) - } else if v.IsOutput() { + in, out := v.IsInput(), v.IsOutput() + if in && out { + return Solution{}, fmt.Errorf("unused input (variable #%d)", i) + } + + if in { + solveHintNIn += nbInstances - len(api.toStore.Dependencies[i]) + } else if out { solveHintNOut += nbInstances } } @@ -138,7 +142,7 @@ func (api *API) Solve(parentApi frontend.API) (Solution, error) { } for i := range circuit { - for _, dep := range circuit[i].Dependencies { + for _, dep := range api.toStore.Dependencies[i] { api.assignments[i][dep.InputInstance] = api.assignments[dep.OutputWire][dep.OutputInstance] } } @@ -152,8 +156,8 @@ func (api *API) Solve(parentApi frontend.API) (Solution, error) { } // Export returns the values of an output variable across all instances -func (s Solution) Export(v frontend.Variable) []frontend.Variable { - return utils.Map(s.permutations.SortedInstances, utils.SliceAt(s.assignments[v.(constraint.GkrVariable)])) +func (s Solution) Export(v gkr.Variable) []frontend.Variable { + return utils.Map(s.permutations.SortedInstances, utils.SliceAt(s.assignments[v])) } // Verify encodes the verification circuitry for the GKR circuit @@ -161,7 +165,7 @@ func (s Solution) Verify(hashName string, initialChallenges ...frontend.Variable var ( err error proofSerialized []frontend.Variable - proof Proof + proof gadget.Proof ) forSnark := newCircuitDataForSnark(s.toStore, s.assignments) @@ -170,7 +174,7 @@ func (s Solution) Verify(hashName string, initialChallenges ...frontend.Variable hintIns := make([]frontend.Variable, len(initialChallenges)+1) // hack: adding one of the outputs of the solve hint to ensure "prove" is called after "solve" for i, w := range s.toStore.Circuit { if w.IsOutput() { - hintIns[0] = s.assignments[i][0] + hintIns[0] = s.assignments[i][len(s.assignments[i])-1] break } } @@ -178,14 +182,14 @@ func (s Solution) Verify(hashName string, initialChallenges ...frontend.Variable proveHintPlaceholder := ProveHintPlaceholder(hashName) if proofSerialized, err = s.parentApi.Compiler().NewHint( - proveHintPlaceholder, ProofSize(forSnark.circuit, logNbInstances), hintIns...); err != nil { + proveHintPlaceholder, gadget.ProofSize(forSnark.circuit, logNbInstances), hintIns...); err != nil { return err } s.toStore.ProveHintID = solver.GetHintID(proveHintPlaceholder) forSnarkSorted := utils.MapRange(0, len(s.toStore.Circuit), slicePtrAt(forSnark.circuit)) - if proof, err = DeserializeProof(forSnarkSorted, proofSerialized); err != nil { + if proof, err = gadget.DeserializeProof(forSnarkSorted, proofSerialized); err != nil { return err } @@ -195,12 +199,12 @@ func (s Solution) Verify(hashName string, initialChallenges ...frontend.Variable } s.toStore.HashName = hashName - err = Verify(s.parentApi, forSnark.circuit, forSnark.assignments, proof, fiatshamir.WithHash(hsh, initialChallenges...), WithSortedCircuit(forSnarkSorted)) + err = gadget.Verify(s.parentApi, forSnark.circuit, forSnark.assignments, proof, fiatshamir.WithHash(hsh, initialChallenges...), gadget.WithSortedCircuit(forSnarkSorted)) if err != nil { return err } - return s.parentApi.Compiler().SetGkrInfo(s.toStore) + return s.parentApi.(gkrinfo.ConstraintSystem).SetGkrInfo(s.toStore) } func slicePtrAt[T any](slice []T) func(int) *T { @@ -216,41 +220,21 @@ func ite[T any](condition bool, ifNot, IfSo T) T { return ifNot } -func newCircuitDataForSnark(info constraint.GkrInfo, assignment assignment) circuitDataForSnark { - circuit := make(Circuit, len(info.Circuit)) - snarkAssignment := make(WireAssignment, len(info.Circuit)) - circuitAt := slicePtrAt(circuit) +func newCircuitDataForSnark(info gkrinfo.StoringInfo, assignment gkrtypes.WireAssignment) circuitDataForSnark { + circuit := make(gkrtypes.Circuit, len(info.Circuit)) + snarkAssignment := make(gkrtypes.WireAssignment, len(info.Circuit)) + for i := range circuit { w := info.Circuit[i] - circuit[i] = Wire{ - Gate: ite(w.IsInput(), Gates[w.Gate], Gate(IdentityGate{})), - Inputs: utils.Map(w.Inputs, circuitAt), - nbUniqueOutputs: w.NbUniqueOutputs, + circuit[i] = gkrtypes.Wire{ + Gate: gkrgates.Get(ite(w.IsInput(), gkr.GateName(w.Gate), gkr.Identity)), + Inputs: w.Inputs, + NbUniqueOutputs: w.NbUniqueOutputs, } - snarkAssignment[&circuit[i]] = assignment[i] + snarkAssignment[i] = assignment[i] } return circuitDataForSnark{ circuit: circuit, assignments: snarkAssignment, } } - -type assignment [][]frontend.Variable - -func (a assignment) NbInstances() int { - for i := range a { - if lenI := len(a[i]); lenI != 0 { - return lenI - } - } - return -1 -} - -func (a assignment) Permute(p constraint.GkrPermutations) { - utils.Permute(a, p.WiresPermutation) - for i := range a { - if a[i] != nil { - utils.Permute(a[i], p.InstancesPermutation) - } - } -} diff --git a/std/gkrapi/compile_test.go b/std/gkrapi/compile_test.go new file mode 100644 index 00000000..a0ca992e --- /dev/null +++ b/std/gkrapi/compile_test.go @@ -0,0 +1,139 @@ +package gkrapi + +import ( + "testing" + + "github.com/consensys/gnark/internal/gkr/gkrinfo" + "github.com/stretchr/testify/assert" +) + +func TestCompile2Cycles(t *testing.T) { + var d = gkrinfo.StoringInfo{ + Dependencies: [][]gkrinfo.InputDependency{ + nil, + { + { + OutputWire: 0, + OutputInstance: 1, + InputInstance: 0, + }, + }, + }, + Circuit: gkrinfo.Circuit{ + { + Inputs: []int{1}, + }, + { + Inputs: []int{}, + }, + }, + } + + expectedCompiled := gkrinfo.StoringInfo{ + Dependencies: [][]gkrinfo.InputDependency{ + {{ + OutputWire: 1, + OutputInstance: 0, + InputInstance: 1, + }}, + nil, + }, + Circuit: gkrinfo.Circuit{ + { + Inputs: []int{}, + NbUniqueOutputs: 1, + }, + { + Inputs: []int{0}, + }}, + NbInstances: 2, + } + + expectedPermutations := gkrinfo.Permutations{ + SortedInstances: []int{1, 0}, + SortedWires: []int{1, 0}, + InstancesPermutation: []int{1, 0}, + WiresPermutation: []int{1, 0}, + } + + p, err := d.Compile(2) + assert.NoError(t, err) + assert.Equal(t, expectedPermutations, p) + assert.Equal(t, expectedCompiled, d) +} + +func TestCompile3Cycles(t *testing.T) { + var d = gkrinfo.StoringInfo{ + Dependencies: [][]gkrinfo.InputDependency{ + nil, + { + { + OutputWire: 0, + OutputInstance: 2, + InputInstance: 0, + }, + { + OutputWire: 0, + OutputInstance: 1, + InputInstance: 2, + }, + }, + nil, + }, + Circuit: gkrinfo.Circuit{ + { + Inputs: []int{2}, + }, + { + Inputs: []int{}, + }, + { + Inputs: []int{1}, + }, + }, + } + + expectedCompiled := gkrinfo.StoringInfo{ + Dependencies: [][]gkrinfo.InputDependency{ + {{ + OutputWire: 2, + OutputInstance: 0, + InputInstance: 1, + }, { + OutputWire: 2, + OutputInstance: 1, + InputInstance: 2, + }}, + + nil, + nil, + }, + Circuit: gkrinfo.Circuit{ + { + Inputs: []int{}, + NbUniqueOutputs: 1, + }, + { + Inputs: []int{0}, + NbUniqueOutputs: 1, + }, + { + Inputs: []int{1}, + NbUniqueOutputs: 0, + }, + }, + NbInstances: 3, // not allowed if we were actually performing gkr + } + + expectedPermutations := gkrinfo.Permutations{ + SortedInstances: []int{1, 2, 0}, + SortedWires: []int{1, 2, 0}, + InstancesPermutation: []int{2, 0, 1}, + WiresPermutation: []int{2, 0, 1}, + } + + p, err := d.Compile(3) + assert.NoError(t, err) + assert.Equal(t, expectedPermutations, p) + assert.Equal(t, expectedCompiled, d) +} diff --git a/std/gkrapi/example_test.go b/std/gkrapi/example_test.go new file mode 100644 index 00000000..29244e6a --- /dev/null +++ b/std/gkrapi/example_test.go @@ -0,0 +1,218 @@ +package gkrapi_test + +import ( + "encoding/binary" + "errors" + + "github.com/consensys/gnark-crypto/ecc" + bls12377 "github.com/consensys/gnark-crypto/ecc/bls12-377" + "github.com/consensys/gnark/constraint/solver/gkrgates" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/gkrapi" + "github.com/consensys/gnark/std/gkrapi/gkr" + _ "github.com/consensys/gnark/std/hash/all" // import all hash functions to register them + "github.com/consensys/gnark/test" +) + +func Example() { + // This example computes the double of multiple BLS12-377 G1 points, which can be computed natively over BW6-761. + // This means that the imported fr and fp packages are the same, being from BW6-761 and BLS12-377 respectively. TODO @Tabaie delete if no longer have fp imported + // It is based on the function DoubleAssign() of type G1Jac in gnark-crypto v0.17.0. + // github.com/consensys/gnark-crypto/ecc/bls12-377 + const fsHashName = "MIMC" + + // register the gates: Doing so is not needed here because + // the proof is being computed in the same session as the + // SNARK circuit being compiled. + // But in production applications it would be necessary. + + assertNoError(gkrgates.Register(squareGate, 1)) + assertNoError(gkrgates.Register(sGate, 4)) + assertNoError(gkrgates.Register(zGate, 4)) + assertNoError(gkrgates.Register(xGate, 2)) + assertNoError(gkrgates.Register(yGate, 4)) + + const nbInstances = 2 + // create instances + assignment := exampleCircuit{ + X: make([]frontend.Variable, nbInstances), + Y: make([]frontend.Variable, nbInstances), + Z: make([]frontend.Variable, nbInstances), + XOut: make([]frontend.Variable, nbInstances), + YOut: make([]frontend.Variable, nbInstances), + ZOut: make([]frontend.Variable, nbInstances), + } + + for i := range nbInstances { + // create a "random" point + var b [8]byte + binary.BigEndian.PutUint64(b[:], uint64(i)) + a, err := bls12377.HashToG1(b[:], nil) + assertNoError(err) + var p bls12377.G1Jac + p.FromAffine(&a) + + assignment.X[i] = p.X + assignment.Y[i] = p.Y + assignment.Z[i] = p.Z + + p.DoubleAssign() + assignment.XOut[i] = p.X + assignment.YOut[i] = p.Y + assignment.ZOut[i] = p.Z + } + + circuit := exampleCircuit{ + X: make([]frontend.Variable, nbInstances), + Y: make([]frontend.Variable, nbInstances), + Z: make([]frontend.Variable, nbInstances), + XOut: make([]frontend.Variable, nbInstances), + YOut: make([]frontend.Variable, nbInstances), + ZOut: make([]frontend.Variable, nbInstances), + fsHashName: fsHashName, + } + + assertNoError(test.IsSolved(&circuit, &assignment, ecc.BW6_761.ScalarField())) + + // Output: +} + +type exampleCircuit struct { + X, Y, Z []frontend.Variable // Jacobian coordinates for each point (input) + XOut, YOut, ZOut []frontend.Variable // Jacobian coordinates for the double of each point (expected output) + fsHashName string // name of the hash function used for Fiat-Shamir in the GKR verifier +} + +func (c *exampleCircuit) Define(api frontend.API) error { + if len(c.X) != len(c.Y) || len(c.X) != len(c.Z) || len(c.X) != len(c.XOut) || len(c.X) != len(c.YOut) || len(c.X) != len(c.ZOut) { + return errors.New("all inputs/outputs must have the same length (i.e. the number of instances)") + } + + gkrApi := gkrapi.New() + + // create GKR circuit variables based on the given assignments + X, err := gkrApi.Import(c.X) + if err != nil { + return err + } + + Y, err := gkrApi.Import(c.Y) + if err != nil { + return err + } + + Z, err := gkrApi.Import(c.Z) + if err != nil { + return err + } + + XX := gkrApi.Gate(squareGate, X) // 405: XX.Square(&p.X) + YY := gkrApi.Gate(squareGate, Y) // 406: YY.Square(&p.Y) + YYYY := gkrApi.Gate(squareGate, YY) // 407: YYYY.Square(&YY) + ZZ := gkrApi.Gate(squareGate, Z) // 408: ZZ.Square(&p.Z) + + S := gkrApi.Gate(sGate, X, YY, XX, YYYY) // 409 - 413 + + // 414: M.Double(&XX).Add(&M, &XX) + // Note (but don't explicitly compute) that M = 3XX + + Z = gkrApi.Gate(zGate, Z, Y, YY, ZZ) // 415 - 418 + X = gkrApi.Gate(xGate, XX, S) // 419-422 + Y = gkrApi.Gate(yGate, S, X, XX, YYYY) // 423 - 426 + + // have to duplicate X for it to be considered an output variable + X = gkrApi.NamedGate(gkr.Identity, X) + + // solve and prove the circuit + solution, err := gkrApi.Solve(api) + if err != nil { + return err + } + + // check the output + + XOut := solution.Export(X) + YOut := solution.Export(Y) + ZOut := solution.Export(Z) + for i := range XOut { + api.AssertIsEqual(XOut[i], c.XOut[i]) + api.AssertIsEqual(YOut[i], c.YOut[i]) + api.AssertIsEqual(ZOut[i], c.ZOut[i]) + } + + challenges := make([]frontend.Variable, 0, len(c.X)*6) + challenges = append(challenges, XOut...) + challenges = append(challenges, YOut...) + challenges = append(challenges, ZOut...) + challenges = append(challenges, c.X...) + challenges = append(challenges, c.Y...) + challenges = append(challenges, c.Z...) + + challenge, err := api.(frontend.Committer).Commit(challenges...) + if err != nil { + return err + } + + // verify the proof + return solution.Verify(c.fsHashName, challenge) +} + +// custom gates + +// squareGate x -> x² +func squareGate(api gkr.GateAPI, input ...frontend.Variable) frontend.Variable { + return api.Mul(input[0], input[0]) +} + +// sGate combines the operations that define the first value assigned to variable S. +// input = [X, YY, XX, YYYY]. +// S = 2 * [(X + YY)² - XX - YYYY]. +func sGate(api gkr.GateAPI, input ...frontend.Variable) (S frontend.Variable) { + S = api.Add(input[0], input[1]) // 409: S.Add(&p.X, &YY) + S = api.Mul(S, S) // 410: S.Square(&S). + S = api.Sub(S, input[2], input[3]) // 411: Sub(&S, &XX). + // 412: Sub(&S, &YYYY). + return api.Add(S, S) // 413: Double(&S) +} + +// zGate combines the operations that define the assignment to p.Z. +// input = [p.Z, p.Y, YY, ZZ]. +// p.Z = (p.Z + p.Y)² - YY - ZZ. +func zGate(api gkr.GateAPI, input ...frontend.Variable) (Z frontend.Variable) { + Z = api.Add(input[0], input[1]) // 415: p.Z.Add(&p.Z, &p.Y). + Z = api.Mul(Z, Z) // 416: p.Z.Square(&p.Z). + Z = api.Sub(Z, input[2], input[3]) // 417: Sub(&p.Z, &YY). + // 418: Sub(&p.Z, &ZZ) + return +} + +// xGate combines the operations that define the assignment to p.X. +// input = [XX, S]. +// p.X = 9XX² - 2S. +func xGate(api gkr.GateAPI, input ...frontend.Variable) (X frontend.Variable) { + M := api.Mul(input[0], 3) // 414: M.Double(&XX).Add(&M, &XX) + T := api.Mul(M, M) // 419: T.Square(&M) + X = api.Sub(T, api.Mul(input[1], 2)) // 420: p.X = T + // 421: T.Double(&S) + // 422: p.X.Sub(&p.X, &T) + return +} + +// yGate combines the operations that define the assignment to p.Y. +// input = [S, p.X, XX, YYYY]. +// p.Y = (S - p.X) * 3 * XX - 8 * YYYY. +func yGate(api gkr.GateAPI, input ...frontend.Variable) (Y frontend.Variable) { + Y = api.Sub(input[0], input[1]) // 423: p.Y.Sub(&S, &p.X). + Y = api.Mul(Y, input[2], 3) // 414: M.Double(&XX).Add(&M, &XX) + // 424:Mul(&p.Y, &M) + Y = api.Sub(Y, api.Mul(input[3], 8)) // 425: YYYY.Double(&YYYY).Double(&YYYY).Double(&YYYY) + // 426: p.Y.Sub(&p.Y, &YYYY) + + return +} + +func assertNoError(err error) { + if err != nil { + panic(err) + } +} diff --git a/std/gkrapi/gkr/types.go b/std/gkrapi/gkr/types.go new file mode 100644 index 00000000..af8a40fc --- /dev/null +++ b/std/gkrapi/gkr/types.go @@ -0,0 +1,66 @@ +package gkr + +import "github.com/consensys/gnark/frontend" + +// Variable represents a value in a GKR circuit. +type Variable int + +// GateAPI is a limited version of frontend.API, +// allowing ring arithmetic operations +type GateAPI interface { + // --------------------------------------------------------------------------------------------- + // Arithmetic + + // Add returns res = i1+i2+...in + Add(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable + + // MulAcc sets and return a = a + (b*c). + // + // ! The method may mutate a without allocating a new result. If the input + // is used elsewhere, then first initialize new variable, for example by + // doing: + // + // acopy := api.Mul(a, 1) + // acopy = api.MulAcc(acopy, b, c) + // + // ! But it may not modify a, always use MulAcc(...) result for correctness. + MulAcc(a, b, c frontend.Variable) frontend.Variable + + // Neg returns -i + Neg(i1 frontend.Variable) frontend.Variable + + // Sub returns res = i1 - i2 - ...in + Sub(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable + + // Mul returns res = i1 * i2 * ... in + Mul(i1, i2 frontend.Variable, in ...frontend.Variable) frontend.Variable + + // Println behaves like fmt.Println but accepts frontend.Variable as parameter + // whose value will be resolved at runtime when computed by the solver + Println(a ...frontend.Variable) +} + +// GateFunction is a function that evaluates a polynomial over its inputs +// using the given GateAPI. +// It is used to define custom gates in GKR circuits. +type GateFunction func(GateAPI, ...frontend.Variable) frontend.Variable + +// GateName is a string representing a (human-readable) name for a GKR gate. +type GateName string + +const ( + // Identity gate: x -> x + Identity GateName = "identity" + + // Add2 gate: (x, y) -> x + y + Add2 GateName = "add2" + + // Sub2 gate: (x, y) -> x - y + Sub2 GateName = "sub2" + + // Neg gate: x -> -x + Neg GateName = "neg" + + // Mul2 gate: (x, y) -> x * y + Mul2 GateName = "mul2" +) diff --git a/std/gkrapi/hints.go b/std/gkrapi/hints.go new file mode 100644 index 00000000..577a4d6e --- /dev/null +++ b/std/gkrapi/hints.go @@ -0,0 +1,137 @@ +package gkrapi + +import ( + "errors" + "fmt" + "math/big" + "strings" + + "github.com/consensys/gnark-crypto/ecc" + gcHash "github.com/consensys/gnark-crypto/hash" + "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/constraint/solver/gkrgates" + bls12377 "github.com/consensys/gnark/internal/gkr/bls12-377" + bls12381 "github.com/consensys/gnark/internal/gkr/bls12-381" + bls24315 "github.com/consensys/gnark/internal/gkr/bls24-315" + bls24317 "github.com/consensys/gnark/internal/gkr/bls24-317" + bn254 "github.com/consensys/gnark/internal/gkr/bn254" + bw6633 "github.com/consensys/gnark/internal/gkr/bw6-633" + bw6761 "github.com/consensys/gnark/internal/gkr/bw6-761" + "github.com/consensys/gnark/internal/gkr/gkrinfo" + "github.com/consensys/gnark/internal/gkr/gkrtypes" + "github.com/consensys/gnark/internal/utils" +) + +var testEngineGkrSolvingData = make(map[string]any) + +func modKey(mod *big.Int) string { + return mod.Text(32) +} + +func SolveHintPlaceholder(gkrInfo gkrinfo.StoringInfo) solver.Hint { + return func(mod *big.Int, ins []*big.Int, outs []*big.Int) error { + + solvingInfo, err := gkrtypes.StoringToSolvingInfo(gkrInfo, gkrgates.Get) + if err != nil { + return err + } + + // TODO @Tabaie autogenerate this or decide not to + if mod.Cmp(ecc.BLS12_377.ScalarField()) == 0 { + var data bls12377.SolvingData + testEngineGkrSolvingData[modKey(mod)] = &data + return bls12377.SolveHint(solvingInfo, &data)(mod, ins, outs) + } + if mod.Cmp(ecc.BLS12_381.ScalarField()) == 0 { + var data bls12381.SolvingData + testEngineGkrSolvingData[modKey(mod)] = &data + return bls12381.SolveHint(solvingInfo, &data)(mod, ins, outs) + } + if mod.Cmp(ecc.BLS24_315.ScalarField()) == 0 { + var data bls24315.SolvingData + testEngineGkrSolvingData[modKey(mod)] = &data + return bls24315.SolveHint(solvingInfo, &data)(mod, ins, outs) + } + if mod.Cmp(ecc.BLS24_317.ScalarField()) == 0 { + var data bls24317.SolvingData + testEngineGkrSolvingData[modKey(mod)] = &data + return bls24317.SolveHint(solvingInfo, &data)(mod, ins, outs) + } + if mod.Cmp(ecc.BN254.ScalarField()) == 0 { + var data bn254.SolvingData + testEngineGkrSolvingData[modKey(mod)] = &data + return bn254.SolveHint(solvingInfo, &data)(mod, ins, outs) + } + if mod.Cmp(ecc.BW6_633.ScalarField()) == 0 { + var data bw6633.SolvingData + testEngineGkrSolvingData[modKey(mod)] = &data + return bw6633.SolveHint(solvingInfo, &data)(mod, ins, outs) + } + if mod.Cmp(ecc.BW6_761.ScalarField()) == 0 { + var data bw6761.SolvingData + testEngineGkrSolvingData[modKey(mod)] = &data + return bw6761.SolveHint(solvingInfo, &data)(mod, ins, outs) + } + + return errors.New("unsupported modulus") + } +} + +func ProveHintPlaceholder(hashName string) solver.Hint { + return func(mod *big.Int, ins, outs []*big.Int) error { + k := modKey(mod) + data, ok := testEngineGkrSolvingData[k] + if !ok { + return errors.New("solving data not found") + } + delete(testEngineGkrSolvingData, k) + + // TODO @Tabaie autogenerate this or decide not to + if mod.Cmp(ecc.BLS12_377.ScalarField()) == 0 { + return bls12377.ProveHint(hashName, data.(*bls12377.SolvingData))(mod, ins, outs) + } + if mod.Cmp(ecc.BLS12_381.ScalarField()) == 0 { + return bls12381.ProveHint(hashName, data.(*bls12381.SolvingData))(mod, ins, outs) + } + if mod.Cmp(ecc.BLS24_315.ScalarField()) == 0 { + return bls24315.ProveHint(hashName, data.(*bls24315.SolvingData))(mod, ins, outs) + } + if mod.Cmp(ecc.BLS24_317.ScalarField()) == 0 { + return bls24317.ProveHint(hashName, data.(*bls24317.SolvingData))(mod, ins, outs) + } + if mod.Cmp(ecc.BN254.ScalarField()) == 0 { + return bn254.ProveHint(hashName, data.(*bn254.SolvingData))(mod, ins, outs) + } + if mod.Cmp(ecc.BW6_633.ScalarField()) == 0 { + return bw6633.ProveHint(hashName, data.(*bw6633.SolvingData))(mod, ins, outs) + } + if mod.Cmp(ecc.BW6_761.ScalarField()) == 0 { + return bw6761.ProveHint(hashName, data.(*bw6761.SolvingData))(mod, ins, outs) + } + + return errors.New("unsupported modulus") + } +} + +func CheckHashHint(hashName string) solver.Hint { + return func(mod *big.Int, ins, outs []*big.Int) error { + if len(ins) != 2 || len(outs) != 1 { + return errors.New("invalid number of inputs/outputs") + } + + toHash := ins[0].Bytes() + expectedHash := ins[1] + + hsh := gcHash.NewHash(fmt.Sprintf("%s_%s", hashName, strings.ToUpper(utils.FieldToCurve(mod).String()))) + hsh.Write(toHash) + hashed := hsh.Sum(nil) + + if hashed := new(big.Int).SetBytes(hashed); hashed.Cmp(expectedHash) != 0 { + return fmt.Errorf("hash mismatch: expected %s, got %s", expectedHash.String(), hashed.String()) + } + + outs[0].SetBytes(hashed) + + return nil + } +} diff --git a/std/gkrapi/testing.go b/std/gkrapi/testing.go new file mode 100644 index 00000000..17163c0b --- /dev/null +++ b/std/gkrapi/testing.go @@ -0,0 +1,120 @@ +package gkrapi + +import ( + "errors" + "fmt" + "sync" + + "github.com/consensys/gnark/constraint/solver/gkrgates" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/utils" + "github.com/consensys/gnark/std/gkrapi/gkr" + stdHash "github.com/consensys/gnark/std/hash" +) + +type solveInTestEngineSettings struct { + hashName string +} + +type SolveInTestEngineOption func(*solveInTestEngineSettings) + +func WithHashName(name string) SolveInTestEngineOption { + return func(s *solveInTestEngineSettings) { + s.hashName = name + } +} + +// SolveInTestEngine solves the defined circuit directly inside the SNARK circuit. This means that the method does not compute the GKR proof of the circuit and does not embed the GKR proof verifier inside a SNARK. +// The output is the values of all variables, across all instances; i.e. indexed variable-first, instance-second. +// This method only works under the test engine and should only be called to debug a GKR circuit, as the GKR prover's errors can be obscure. +func (api *API) SolveInTestEngine(parentApi frontend.API, options ...SolveInTestEngineOption) [][]frontend.Variable { + gateVer, err := gkrgates.NewGateVerifier(utils.FieldToCurve(parentApi.Compiler().Field())) + if err != nil { + panic(err) + } + + var s solveInTestEngineSettings + for _, o := range options { + o(&s) + } + if s.hashName != "" { + // hash something and make sure it gives the same answer both on prover and verifier sides + // TODO @Tabaie If indeed cheap, move this feature to Verify so that it is always run + h, err := stdHash.GetFieldHasher(s.hashName, parentApi) + if err != nil { + panic(err) + } + nbBytes := (parentApi.Compiler().FieldBitLen() + 7) / 8 + toHash := frontend.Variable(0) + for i := range nbBytes { + toHash = parentApi.Add(parentApi.Mul(toHash, 256), i%256) + } + h.Reset() + h.Write(toHash) + hashed := h.Sum() + + hintOut, err := parentApi.Compiler().NewHint(CheckHashHint(s.hashName), 1, toHash, hashed) + if err != nil { + panic(err) + } + parentApi.AssertIsEqual(hintOut[0], hashed) // the hint already checks this + } + + res := make([][]frontend.Variable, len(api.toStore.Circuit)) + var verifiedGates sync.Map + for i, w := range api.toStore.Circuit { + res[i] = make([]frontend.Variable, api.nbInstances()) + copy(res[i], api.assignments[i]) + if len(w.Inputs) == 0 { + continue + } + } + for instanceI := range api.nbInstances() { + for wireI, w := range api.toStore.Circuit { + deps := api.toStore.Dependencies[wireI] + if len(deps) != 0 && len(w.Inputs) != 0 { + panic(fmt.Errorf("non-input wire %d should not have dependencies", wireI)) + } + for _, dep := range deps { + if dep.InputInstance == instanceI { + if dep.OutputInstance >= instanceI { + panic(fmt.Errorf("out of order dependency not yet supported in SolveInTestEngine; (wire %d, instance %d) depends on (wire %d, instance %d)", wireI, instanceI, dep.OutputWire, dep.OutputInstance)) + } + if res[wireI][instanceI] != nil { + panic(fmt.Errorf("dependency (wire %d, instance %d) <- (wire %d, instance %d) attempting to override existing value assignment", wireI, instanceI, dep.OutputWire, dep.OutputInstance)) + } + res[wireI][instanceI] = res[dep.OutputWire][dep.OutputInstance] + } + } + + if res[wireI][instanceI] == nil { // no assignment or dependency + if len(w.Inputs) == 0 { + panic(fmt.Errorf("input wire %d, instance %d has no dependency or explicit assignment", wireI, instanceI)) + } + ins := make([]frontend.Variable, len(w.Inputs)) + for i, in := range w.Inputs { + ins[i] = res[in][instanceI] + } + gate := gkrgates.Get(gkr.GateName(w.Gate)) + if gate == nil && !w.IsInput() { + panic(fmt.Errorf("gate %s not found", w.Gate)) + } + if _, ok := verifiedGates.Load(w.Gate); !ok { + verifiedGates.Store(w.Gate, struct{}{}) + + err = errors.Join( + gateVer.VerifyDegree(gate), + gateVer.VerifySolvability(gate), + ) + if err != nil { + panic(fmt.Errorf("gate %s: %w", w.Gate, err)) + } + } + if gate != nil { + res[wireI][instanceI] = gate.Evaluate(parentApi, ins...) + } + } + } + } + return res +} diff --git a/std/hash/all/allhashes.go b/std/hash/all/allhashes.go new file mode 100644 index 00000000..6dc2716b --- /dev/null +++ b/std/hash/all/allhashes.go @@ -0,0 +1,6 @@ +package all + +import ( + _ "github.com/consensys/gnark/std/hash/mimc" + _ "github.com/consensys/gnark/std/hash/poseidon2" +) diff --git a/std/hash/hash.go b/std/hash/hash.go index c1f6fe13..c077fd0d 100644 --- a/std/hash/hash.go +++ b/std/hash/hash.go @@ -5,9 +5,6 @@ package hash import ( - "fmt" - "sync" - "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/std/math/uints" ) @@ -42,27 +39,6 @@ type StateStorer interface { SetState(state []frontend.Variable) error } -var ( - builderRegistry = make(map[string]func(api frontend.API) (FieldHasher, error)) - lock sync.RWMutex -) - -func Register(name string, builder func(api frontend.API) (FieldHasher, error)) { - lock.Lock() - defer lock.Unlock() - builderRegistry[name] = builder -} - -func GetFieldHasher(name string, api frontend.API) (FieldHasher, error) { - lock.RLock() - defer lock.RUnlock() - builder, ok := builderRegistry[name] - if !ok { - return nil, fmt.Errorf("hash function \"%s\" not registered", name) - } - return builder(api) -} - // BinaryHasher hashes inputs into a short digest. It takes as inputs bytes and // outputs byte array whose length depends on the underlying hash function. For // SNARK-native hash functions use [FieldHasher]. @@ -84,10 +60,34 @@ type BinaryHasher interface { // the length of the input is the total number of bytes written. type BinaryFixedLengthHasher interface { BinaryHasher - // FixedLengthSum returns digest of the first length bytes. + // FixedLengthSum returns digest of the first length bytes. See the + // [WithMinimalLength] option for setting lower bound on length. FixedLengthSum(length frontend.Variable) []uints.U8 } +// HasherConfig allows to configure the behavior of the hash constructors. Do +// not initialize the configuration directly but rather use the [Option] +// functions which perform correct initializations. This configuration is +// exported for importing in hash implementations. +type HasherConfig struct { + MinimalLength int +} + +// Option allows configuring the hash functions. +type Option func(*HasherConfig) error + +// WithMinimalLength hints the minimal length of the input to the hash function. +// This allows to optimize the constraint count when calling +// [BinaryFixedLengthHasher.FixedLengthSum] as we can avoid selecting between +// the dummy padding and actual padding. If this option is not provided, then we +// assume the minimal length is 0. +func WithMinimalLength(minimalLength int) Option { + return func(cfg *HasherConfig) error { + cfg.MinimalLength = minimalLength + return nil + } +} + // Compressor is a 2-1 one-way function. It takes two inputs and compresses // them into one output. // diff --git a/std/hash/mimc/mimc.go b/std/hash/mimc/mimc.go index 530fb658..9d8a98e3 100644 --- a/std/hash/mimc/mimc.go +++ b/std/hash/mimc/mimc.go @@ -1,92 +1,47 @@ -// Copyright 2020-2025 Consensys Software Inc. -// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. - package mimc import ( - "errors" - "math/big" - - "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/internal/utils" + "github.com/consensys/gnark/std/hash" + "github.com/consensys/gnark/std/internal/mimc" ) -// MiMC contains the params of the MiMC hash func and the curves on which it is implemented. +// MiMC contains the params of the MiMC hash func and the curves on which it is +// implemented. The reference to this type implements [hash.FieldHasher]. // // NB! See the package documentation for length extension attack consideration. -type MiMC struct { - params []big.Int // slice containing constants for the encryption rounds - id ecc.ID // id needed to know which encryption function to use - h frontend.Variable // current vector in the Miyaguchi–Preneel scheme - data []frontend.Variable // state storage. data is updated when Write() is called. Sum sums the data. - api frontend.API // underlying constraint system -} +type MiMC = mimc.MiMC // NewMiMC returns a MiMC instance that can be used in a gnark circuit. The -// out-circuit counterpart of this function is provided in [gnark-crypto]. +// out-circuit counterpart of this function is provided in [gnark-crypto]. The +// reference to the returned type implements [hash.FieldHasher], but we keep the +// method for backwards compatibility. See also [New]. // // NB! See the package documentation for length extension attack consideration. // // [gnark-crypto]: https://pkg.go.dev/github.com/consensys/gnark-crypto/hash func NewMiMC(api frontend.API) (MiMC, error) { - // TODO @gbotrel use field - if constructor, ok := newMimc[utils.FieldToCurve(api.Compiler().Field())]; ok { - return constructor(api), nil + h, err := mimc.NewMiMC(api) + if err != nil { + return MiMC{}, err } - return MiMC{}, errors.New("unknown curve id") + return h, nil } -// Write adds more data to the running hash. -func (h *MiMC) Write(data ...frontend.Variable) { - h.data = append(h.data, data...) -} - -// Reset resets the Hash to its initial state. -func (h *MiMC) Reset() { - h.data = nil - h.h = 0 -} - -// SetState manually sets the state of the hasher to the provided value. In the -// case of MiMC only a single frontend variable is expected to represent the -// state. -func (h *MiMC) SetState(newState []frontend.Variable) error { - - if len(h.data) > 0 { - return errors.New("the hasher is not in an initial state") - } - - if len(newState) != 1 { - return errors.New("the MiMC hasher expects a single field element to represent the state") - } - - h.h = newState[0] - h.data = nil - return nil -} - -// State returns the inner-state of the hasher. In the context of MiMC only a -// single field element is returned. -func (h *MiMC) State() []frontend.Variable { - h.Sum() // this flushes the unsummed data - return []frontend.Variable{h.h} -} - -// Sum hash using [Miyaguchi–Preneel] where the XOR operation is replaced by -// field addition. +// New returns a new MiMC hasher that can be used in a gnark circuit. The +// out-circuit counterpart of this function is provided in [gnark-crypto]. // -// [Miyaguchi–Preneel]: https://en.wikipedia.org/wiki/One-way_compression_function -func (h *MiMC) Sum() frontend.Variable { - - //h.Write(data...)s - for _, stream := range h.data { - r := encryptFuncs[h.id](*h, stream) - h.h = h.api.Add(h.h, r, stream) +// NB! See the package documentation for length extension attack consideration. +// +// [gnark-crypto]: https://pkg.go.dev/github.com/consensys/gnark-crypto/hash +func New(api frontend.API) (hash.FieldHasher, error) { + h, err := NewMiMC(api) + if err != nil { + return nil, err } + return &h, nil +} - h.data = nil // flush the data already hashed - - return h.h - +func init() { + hash.Register(hash.MIMC, New) } diff --git a/std/hash/poseidon2/poseidon2.go b/std/hash/poseidon2/poseidon2.go index d0427d2c..804740ff 100644 --- a/std/hash/poseidon2/poseidon2.go +++ b/std/hash/poseidon2/poseidon2.go @@ -5,7 +5,7 @@ import ( "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/std/hash" - poseidon2 "github.com/consensys/gnark/std/permutation/poseidon2" + "github.com/consensys/gnark/std/permutation/poseidon2" ) // NewMerkleDamgardHasher returns a Poseidon2 hasher using the Merkle-Damgard @@ -17,3 +17,7 @@ func NewMerkleDamgardHasher(api frontend.API) (hash.FieldHasher, error) { } return hash.NewMerkleDamgardHasher(api, f, 0), nil } + +func init() { + hash.Register(hash.POSEIDON2, NewMerkleDamgardHasher) +} diff --git a/std/hash/registry.go b/std/hash/registry.go new file mode 100644 index 00000000..0d037e1f --- /dev/null +++ b/std/hash/registry.go @@ -0,0 +1,114 @@ +package hash + +import ( + "fmt" + "sync" + + "github.com/consensys/gnark/frontend" +) + +var ( + defaultHashes = make([]func(api frontend.API) (FieldHasher, error), maxHash) + namedHashes = make(map[string]func(api frontend.API) (FieldHasher, error), maxHash) + lock sync.RWMutex +) + +// Hash represents a registered hash function. +type Hash uint + +const ( + // MIMC is the MiMC hash function over the native field of the curve. + MIMC Hash = iota + // POSEIDON2 is the Poseidon2 hash function over the native field of the curve. + POSEIDON2 + + maxHash // the number of registered hash functions +) + +// New initializes the hash function. This is a convenience function which does +// not allow setting hash-specific options. +func (m Hash) New(api frontend.API) (FieldHasher, error) { + if m < maxHash { + lock.RLock() + defer lock.RUnlock() + builder := defaultHashes[m] + if builder != nil { + return builder(api) + } + } + return nil, fmt.Errorf("hash function \"%s\" not registered. Import the corresponding hash function package", m) +} + +// Returns the unique identifier of the hash function as a string. +func (m Hash) String() string { + switch m { + case MIMC: + return "MIMC" + case POSEIDON2: + return "POSEIDON2" + default: + return fmt.Sprintf("unknown hash function %d", m) + } +} + +// Available returns true if the hash function is available. +func (m Hash) Available() bool { + return m < maxHash && defaultHashes[m] != nil +} + +// Register registers a new hash function by its constant index. To ensure that +// the hash function is registered, import the corresponding hash gadget package +// so that it would call this method. +// +// Alternatively, you can import the [github.com/consensys/gnark/std/hash/all] +// package which automatically registers all hash functions. +func Register(m Hash, builder func(api frontend.API) (FieldHasher, error)) { + if m >= maxHash { + panic(fmt.Sprintf("cannot register a hash function with index %d, maximum is %d", m, maxHash-1)) + } + lock.Lock() + defer lock.Unlock() + defaultHashes[m] = builder +} + +// RegisterCustomHash registers a new hash function by a name. To ensure that +// the hash function is registered, import the corresponding hash gadget package +// so that it would call this method. +// +// Alternatively, you can import the [github.com/consensys/gnark/std/hash/all] +// package which automatically registers all hash functions. +func RegisterCustomHash(name string, builder func(api frontend.API) (FieldHasher, error)) { + for i := Hash(0); i < maxHash; i++ { + if i.String() == name { + panic("cannot register a named hash overriding a default hash function: " + name) + } + } + lock.Lock() + defer lock.Unlock() + namedHashes[name] = builder +} + +// GetFieldHasher retrieves a hash function by its name. The name should match +// the output of [Hash.String] or name used in [RegisterCustomHash] method. To +// ensure that the hash function is correctly registered (and thus available for +// getting with this method), import the corresponding hash gadget package so +// that it would call the [Register] or [RegisterCustomHash] method. +// +// Alternatively, you can import the [github.com/consensys/gnark/std/hash/all] +// package which automatically registers all hash functions. +func GetFieldHasher(name string, api frontend.API) (FieldHasher, error) { + lock.RLock() + defer lock.RUnlock() + for i := Hash(0); i < maxHash; i++ { + if i.String() == name { + builder := defaultHashes[i] + if builder != nil { + return builder(api) + } + } + } + if f, ok := namedHashes[name]; ok { + return f(api) + } + panic(fmt.Sprintf("hash function \"%s\" not registered. Import the corresponding package to register it", name)) +} diff --git a/std/hash/sha2/sha2.go b/std/hash/sha2/sha2.go index ea36f7f7..4faf8577 100644 --- a/std/hash/sha2/sha2.go +++ b/std/hash/sha2/sha2.go @@ -6,6 +6,7 @@ package sha2 import ( "encoding/binary" + "fmt" "math/big" "github.com/consensys/gnark/frontend" @@ -25,14 +26,22 @@ type digest struct { api frontend.API uapi *uints.BinaryField[uints.U32] in []uints.U8 + + minimalLength int } -func New(api frontend.API) (hash.BinaryFixedLengthHasher, error) { +func New(api frontend.API, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) { + cfg := new(hash.HasherConfig) + for _, opt := range opts { + if err := opt(cfg); err != nil { + return nil, fmt.Errorf("applying option: %w", err) + } + } uapi, err := uints.New[uints.U32](api) if err != nil { - return nil, err + return nil, fmt.Errorf("initializing uints: %w", err) } - return &digest{api: api, uapi: uapi}, nil + return &digest{api: api, uapi: uapi, minimalLength: cfg.MinimalLength}, nil } func (d *digest) Write(data []uints.U8) { @@ -68,9 +77,14 @@ func (d *digest) Sum() []uints.U8 { copy(buf[:], padded[i*64:(i+1)*64]) runningDigest = sha2.Permute(d.uapi, runningDigest, buf) } + + return d.unpackU8Digest(runningDigest) +} + +func (d *digest) unpackU8Digest(digest [8]uints.U32) []uints.U8 { var ret []uints.U8 - for i := range runningDigest { - ret = append(ret, d.uapi.UnpackMSB(runningDigest[i])...) + for i := range digest { + ret = append(ret, d.uapi.UnpackMSB(digest[i])...) } return ret } @@ -85,15 +99,18 @@ func (d *digest) FixedLengthSum(length frontend.Variable) []uints.U8 { // idea - have a mask for blocks where 1 is only for the block we want to // use. - data := make([]uints.U8, len(d.in)) - copy(data, d.in) - - comparator := cmp.NewBoundedComparator(d.api, big.NewInt(int64(len(data)+64+8)), false) - - for i := 0; i < 64+8; i++ { - data = append(data, uints.NewU8(0)) + maxLen := len(d.in) + comparator := cmp.NewBoundedComparator(d.api, big.NewInt(int64(maxLen+64+8)), false) + // when minimal length is 0 (i.e. not set), then we can skip the check as it holds naturally (all field elements are non-negative) + if d.minimalLength > 0 { + // we use comparator as [frontend.API] doesn't have a fast path for case API.AssertIsLessOrEqual(constant, variable) + comparator.AssertIsLessEq(d.minimalLength, length) } + data := make([]uints.U8, maxLen) + copy(data, d.in) + data = append(data, uints.NewU8Array(make([]uint8, 64+8))...) + lenMod64 := d.mod64(length) lenMod64Less56 := comparator.IsLess(lenMod64, 56) @@ -106,16 +123,18 @@ func (d *digest) FixedLengthSum(length frontend.Variable) []uints.U8 { var dataLenBtyes [8]frontend.Variable d.bigEndianPutUint64(dataLenBtyes[:], d.api.Mul(length, 8)) - for i := range data { - isPaddingStartPos := d.api.IsZero(d.api.Sub(i, length)) + // When i < minLen or i > maxLen, padding 1 0r 0 is completely unnecessary + for i := d.minimalLength; i <= maxLen; i++ { + isPaddingStartPos := cmp.IsEqual(d.api, i, length) data[i].Val = d.api.Select(isPaddingStartPos, 0x80, data[i].Val) isPaddingPos := comparator.IsLess(length, i) data[i].Val = d.api.Select(isPaddingPos, 0, data[i].Val) } - for i := range data { - isLast8BytesPos := d.api.IsZero(d.api.Sub(i, last8BytesPos)) + // When i <= minLen, padding length is completely unnecessary + for i := d.minimalLength + 1; i < len(data); i++ { + isLast8BytesPos := cmp.IsEqual(d.api, i, last8BytesPos) for j := 0; j < 8; j++ { if i+j < len(data) { data[i+j].Val = d.api.Select(isLast8BytesPos, dataLenBtyes[j], data[i+j].Val) @@ -127,14 +146,20 @@ func (d *digest) FixedLengthSum(length frontend.Variable) []uints.U8 { var resultDigest [8]uints.U32 var buf [64]uints.U8 copy(runningDigest[:], _seed) - copy(resultDigest[:], _seed) for i := 0; i < len(data)/64; i++ { copy(buf[:], data[i*64:(i+1)*64]) runningDigest = sha2.Permute(d.uapi, runningDigest, buf) - isInRange := comparator.IsLess(i*64, totalLen) + // When i < minLen/64, runningDigest cannot be resultDigest, and proceed to the next loop directly + if i < d.minimalLength/64 { + continue + } else if i == d.minimalLength/64 { // init resultDigests + copy(resultDigest[:], runningDigest[:]) + continue + } + isInRange := comparator.IsLess(i*64, totalLen) for j := 0; j < 8; j++ { for k := 0; k < 4; k++ { resultDigest[j][k].Val = d.api.Select(isInRange, runningDigest[j][k].Val, resultDigest[j][k].Val) @@ -142,11 +167,7 @@ func (d *digest) FixedLengthSum(length frontend.Variable) []uints.U8 { } } - var ret []uints.U8 - for i := range resultDigest { - ret = append(ret, d.uapi.UnpackMSB(resultDigest[i])...) - } - return ret + return d.unpackU8Digest(resultDigest) } func (d *digest) Reset() { diff --git a/std/hash/sha2/sha2_test.go b/std/hash/sha2/sha2_test.go index 0093fddc..5f3572ac 100644 --- a/std/hash/sha2/sha2_test.go +++ b/std/hash/sha2/sha2_test.go @@ -1,12 +1,14 @@ package sha2 import ( + "crypto/rand" "crypto/sha256" "fmt" "testing" "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/hash" "github.com/consensys/gnark/std/math/uints" "github.com/consensys/gnark/test" ) @@ -53,10 +55,13 @@ type sha2FixedLengthCircuit struct { In []uints.U8 Length frontend.Variable Expected [32]uints.U8 + + // minimal length of the input is the circuit parameter + minimalLength int } func (c *sha2FixedLengthCircuit) Define(api frontend.API) error { - h, err := New(api) + h, err := New(api, hash.WithMinimalLength(c.minimalLength)) if err != nil { return err } @@ -76,16 +81,30 @@ func (c *sha2FixedLengthCircuit) Define(api frontend.API) error { } func TestSHA2FixedLengthSum(t *testing.T) { - bts := make([]byte, 144) - length := 56 - dgst := sha256.Sum256(bts[:length]) - witness := sha2FixedLengthCircuit{ - In: uints.NewU8Array(bts), - Length: length, - } - copy(witness.Expected[:], uints.NewU8Array(dgst[:])) - err := test.IsSolved(&sha2FixedLengthCircuit{In: make([]uints.U8, len(bts))}, &witness, ecc.BN254.ScalarField()) - if err != nil { - t.Fatal(err) + const maxLen = 144 + assert := test.NewAssert(t) + bts := make([]byte, maxLen) + _, err := rand.Reader.Read(bts) + assert.NoError(err) + + for _, lengthBound := range []int{0, 1, 63, 64, 65, len(bts)} { + circuit := &sha2FixedLengthCircuit{In: make([]uints.U8, len(bts)), minimalLength: lengthBound} + for _, length := range []int{0, 1, 63, 64, 65, len(bts)} { + assert.Run(func(assert *test.Assert) { + dgst := sha256.Sum256(bts[:length]) + witness := &sha2FixedLengthCircuit{ + In: uints.NewU8Array(bts), + Length: length, + Expected: [32]uints.U8(uints.NewU8Array(dgst[:])), + } + + err = test.IsSolved(circuit, witness, ecc.BN254.ScalarField()) + if length >= lengthBound { + assert.NoError(err) + } else if length < lengthBound { + assert.Error(err, "expected error for length < lengthBound") + } + }, fmt.Sprintf("bound=%d/length=%d", lengthBound, length)) + } } } diff --git a/std/hash/sha3/hashes.go b/std/hash/sha3/hashes.go index 4fac3289..38273f9e 100644 --- a/std/hash/sha3/hashes.go +++ b/std/hash/sha3/hashes.go @@ -1,99 +1,69 @@ package sha3 import ( + "fmt" + "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/std/hash" "github.com/consensys/gnark/std/math/uints" ) -// New256 creates a new SHA3-256 hash. -// Its generic security strength is 256 bits against preimage attacks, -// and 128 bits against collision attacks. -func New256(api frontend.API) (hash.BinaryFixedLengthHasher, error) { +// newHash is a helper function to create a new SHA3 hash. +func newHash(api frontend.API, dsByte byte, rate, outputLen int, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) { + cfg := new(hash.HasherConfig) + for _, opt := range opts { + if err := opt(cfg); err != nil { + return nil, fmt.Errorf("applying option: %w", err) + } + } uapi, err := uints.New[uints.U64](api) if err != nil { - return nil, err + return nil, fmt.Errorf("initializing uints: %w", err) } return &digest{ - api: api, - uapi: uapi, - state: newState(), - dsbyte: 0x06, - rate: 136, - outputLen: 32, + api: api, + uapi: uapi, + state: newState(), + dsbyte: dsByte, + rate: rate, + outputLen: outputLen, + minimalLength: cfg.MinimalLength, }, nil } +// New256 creates a new SHA3-256 hash. +// Its generic security strength is 256 bits against preimage attacks, +// and 128 bits against collision attacks. +func New256(api frontend.API, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) { + return newHash(api, 0x06, 136, 32, opts...) +} + // New384 creates a new SHA3-384 hash. // Its generic security strength is 384 bits against preimage attacks, // and 192 bits against collision attacks. -func New384(api frontend.API) (hash.BinaryFixedLengthHasher, error) { - uapi, err := uints.New[uints.U64](api) - if err != nil { - return nil, err - } - return &digest{ - api: api, - uapi: uapi, - state: newState(), - dsbyte: 0x06, - rate: 104, - outputLen: 48, - }, nil +func New384(api frontend.API, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) { + return newHash(api, 0x06, 104, 48, opts...) } // New512 creates a new SHA3-512 hash. // Its generic security strength is 512 bits against preimage attacks, // and 256 bits against collision attacks. -func New512(api frontend.API) (hash.BinaryFixedLengthHasher, error) { - uapi, err := uints.New[uints.U64](api) - if err != nil { - return nil, err - } - return &digest{ - api: api, - uapi: uapi, - state: newState(), - dsbyte: 0x06, - rate: 72, - outputLen: 64, - }, nil +func New512(api frontend.API, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) { + return newHash(api, 0x06, 72, 64, opts...) } // NewLegacyKeccak256 creates a new Keccak-256 hash. // // Only use this function if you require compatibility with an existing cryptosystem // that uses non-standard padding. All other users should use New256 instead. -func NewLegacyKeccak256(api frontend.API) (hash.BinaryFixedLengthHasher, error) { - uapi, err := uints.New[uints.U64](api) - if err != nil { - return nil, err - } - return &digest{ - api: api, - uapi: uapi, - state: newState(), - dsbyte: 0x01, - rate: 136, - outputLen: 32, - }, nil +func NewLegacyKeccak256(api frontend.API, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) { + return newHash(api, 0x01, 136, 32, opts...) } // NewLegacyKeccak512 creates a new Keccak-512 hash. // // Only use this function if you require compatibility with an existing cryptosystem // that uses non-standard padding. All other users should use New512 instead. -func NewLegacyKeccak512(api frontend.API) (hash.BinaryFixedLengthHasher, error) { - uapi, err := uints.New[uints.U64](api) - if err != nil { - return nil, err - } - return &digest{ - api: api, - uapi: uapi, - state: newState(), - dsbyte: 0x01, - rate: 72, - outputLen: 64, - }, nil +func NewLegacyKeccak512(api frontend.API, opts ...hash.Option) (hash.BinaryFixedLengthHasher, error) { + return newHash(api, 0x01, 72, 64, opts...) } diff --git a/std/hash/sha3/sha3.go b/std/hash/sha3/sha3.go index be5ba4df..cc6152ff 100644 --- a/std/hash/sha3/sha3.go +++ b/std/hash/sha3/sha3.go @@ -10,13 +10,14 @@ import ( ) type digest struct { - api frontend.API - uapi *uints.BinaryField[uints.U64] - state [25]uints.U64 // 1600 bits state: 25 x 64 - in []uints.U8 // input to be digested - dsbyte byte // dsbyte contains the "domain separation" bits and the first bit of the padding - rate int // the number of bytes of state to use - outputLen int // the default output size in bytes + api frontend.API + uapi *uints.BinaryField[uints.U64] + state [25]uints.U64 // 1600 bits state: 25 x 64 + in []uints.U8 // input to be digested + dsbyte byte // dsbyte contains the "domain separation" bits and the first bit of the padding + rate int // the number of bytes of state to use + outputLen int // the default output size in bytes + minimalLength int // lower bound on the length of the input to optimize fixed length hashing } func (d *digest) Write(in []uints.U8) { @@ -39,10 +40,18 @@ func (d *digest) Sum() []uints.U8 { } func (d *digest) FixedLengthSum(length frontend.Variable) []uints.U8 { + comparator := cmp.NewBoundedComparator(d.api, big.NewInt(int64(len(d.in))), false) + // in case the lower bound on the length of input is given, check that the input is long enough + if d.minimalLength > 0 { + comparator.AssertIsLessEq(d.minimalLength, length) + } + padded, numberOfBlocks := d.paddingFixedWidth(length) blocks := d.composeBlocks(padded) + d.absorbingFixedWidth(blocks, numberOfBlocks) + return d.squeezeBlocks() } @@ -67,11 +76,13 @@ func (d *digest) padding() []uints.U8 { func (d *digest) paddingFixedWidth(length frontend.Variable) (padded []uints.U8, numberOfBlocks frontend.Variable) { numberOfBlocks = frontend.Variable(0) - padded = make([]uints.U8, len(d.in)) + maxLen := len(d.in) + padded = make([]uints.U8, maxLen) copy(padded[:], d.in[:]) padded = append(padded, uints.NewU8Array(make([]uint8, d.rate))...) - for i := 0; i <= len(padded)-d.rate; i++ { + // When i < minLen or i > maxLen, it is completely unnecessary + for i := d.minimalLength; i <= maxLen; i++ { reachEnd := cmp.IsEqual(d.api, i, length) switch q := d.rate - ((i) % d.rate); q { case 1: @@ -83,7 +94,7 @@ func (d *digest) paddingFixedWidth(length frontend.Variable) (padded []uints.U8, numberOfBlocks = d.api.Select(reachEnd, (i+2)/d.rate, numberOfBlocks) default: padded[i].Val = d.api.Select(reachEnd, d.dsbyte, padded[i].Val) - for j := 0; j < q-1; j++ { + for j := 0; j < q-2; j++ { padded[i+1+j].Val = d.api.Select(reachEnd, 0, padded[i+1+j].Val) } padded[i+q-1].Val = d.api.Select(reachEnd, 0x80, padded[i+q-1].Val) @@ -119,9 +130,9 @@ func (d *digest) absorbing(blocks [][]uints.U64) { } func (d *digest) absorbingFixedWidth(blocks [][]uints.U64, nbBlocks frontend.Variable) { + minNbOfBlocks := d.minimalLength / d.rate var state [25]uints.U64 var resultState [25]uints.U64 - copy(resultState[:], d.state[:]) copy(state[:], d.state[:]) comparator := cmp.NewBoundedComparator(d.api, big.NewInt(int64(len(blocks))), false) @@ -131,9 +142,18 @@ func (d *digest) absorbingFixedWidth(blocks [][]uints.U64, nbBlocks frontend.Var state[j] = d.uapi.Xor(state[j], block[j]) } state = keccakf.Permute(d.uapi, state) + + // When i < minNbOfBlocks, state cannot be resultState, and proceed to the next loop directly + if i < minNbOfBlocks { + continue + } else if i == minNbOfBlocks { // init resultState + copy(resultState[:], state[:]) + continue + } + isInRange := comparator.IsLess(i, nbBlocks) - // only select blocks that are in range - for j := 0; j < 25; j++ { + // only select blocks that are in range. Only process the first outputLen data relevant to the result + for j := 0; j < d.outputLen/8; j++ { for k := 0; k < 8; k++ { resultState[j][k].Val = d.api.Select(isInRange, state[j][k].Val, resultState[j][k].Val) } diff --git a/std/hash/sha3/sha3_test.go b/std/hash/sha3/sha3_test.go index eeec0913..9e8e7c75 100644 --- a/std/hash/sha3/sha3_test.go +++ b/std/hash/sha3/sha3_test.go @@ -16,7 +16,7 @@ import ( ) type testCase struct { - zk func(api frontend.API) (zkhash.BinaryFixedLengthHasher, error) + zk func(api frontend.API, opts ...zkhash.Option) (zkhash.BinaryFixedLengthHasher, error) native func() hash.Hash } @@ -95,6 +95,9 @@ type sha3FixedLengthSumCircuit struct { Expected []uints.U8 Length frontend.Variable hasher string + + // minimal length of the input is the circuit parameter + minimalLength int } func (c *sha3FixedLengthSumCircuit) Define(api frontend.API) error { @@ -102,7 +105,7 @@ func (c *sha3FixedLengthSumCircuit) Define(api frontend.API) error { if !ok { return fmt.Errorf("hash function unknown: %s", c.hasher) } - h, err := newHasher.zk(api) + h, err := newHasher.zk(api, zkhash.WithMinimalLength(c.minimalLength)) if err != nil { return err } @@ -120,8 +123,9 @@ func (c *sha3FixedLengthSumCircuit) Define(api frontend.API) error { } func TestSHA3FixedLengthSum(t *testing.T) { + const maxLen = 310 assert := test.NewAssert(t) - in := make([]byte, 310) + in := make([]byte, maxLen) _, err := rand.Reader.Read(in) assert.NoError(err) @@ -129,29 +133,33 @@ func TestSHA3FixedLengthSum(t *testing.T) { assert.Run(func(assert *test.Assert) { name := name strategy := testCases[name] - for _, length := range []int{0, 1, 31, 32, 33, 135, 136, 137, len(in)} { - assert.Run(func(assert *test.Assert) { - h := strategy.native() - h.Write(in[:length]) - expected := h.Sum(nil) - - circuit := &sha3FixedLengthSumCircuit{ - In: make([]uints.U8, len(in)), - Expected: make([]uints.U8, len(expected)), - Length: 0, - hasher: name, - } - - witness := &sha3FixedLengthSumCircuit{ - In: uints.NewU8Array(in), - Expected: uints.NewU8Array(expected), - Length: length, - } - - if err := test.IsSolved(circuit, witness, ecc.BN254.ScalarField()); err != nil { - t.Fatalf("%s: %s", name, err) - } - }, fmt.Sprintf("length=%d", length)) + nHasher := strategy.native() + for _, lengthBound := range []int{0, 1, nHasher.BlockSize() - 1, nHasher.BlockSize(), nHasher.BlockSize() + 1, len(in)} { + circuit := &sha3FixedLengthSumCircuit{ + In: make([]uints.U8, len(in)), + Expected: make([]uints.U8, nHasher.Size()), + hasher: name, + minimalLength: lengthBound, + } + for _, length := range []int{0, 1, nHasher.BlockSize() - 1, nHasher.BlockSize(), nHasher.BlockSize() + 1, len(in)} { + assert.Run(func(assert *test.Assert) { + h := strategy.native() + h.Write(in[:length]) + expected := h.Sum(nil) + + witness := &sha3FixedLengthSumCircuit{ + In: uints.NewU8Array(in), + Expected: uints.NewU8Array(expected), + Length: length, + } + err := test.IsSolved(circuit, witness, ecc.BN254.ScalarField()) + if length >= lengthBound { + assert.NoError(err) + } else if length < lengthBound { + assert.Error(err, "expected error for length < lengthBound") + } + }, fmt.Sprintf("bound=%d/length=%d", lengthBound, length)) + } } }, fmt.Sprintf("hash=%s", name)) } diff --git a/std/internal/limbcomposition/composition_test.go b/std/internal/limbcomposition/composition_test.go index f00044e1..dc4f37fe 100644 --- a/std/internal/limbcomposition/composition_test.go +++ b/std/internal/limbcomposition/composition_test.go @@ -7,6 +7,7 @@ import ( "reflect" "testing" + "github.com/consensys/gnark-crypto/field/babybear" limbs "github.com/consensys/gnark/std/internal/limbcomposition" "github.com/consensys/gnark/std/math/emulated" "github.com/consensys/gnark/std/math/emulated/emparams" @@ -24,6 +25,7 @@ func testComposition[T emulated.FieldParams](t *testing.T) { t.Helper() assert := test.NewAssert(t) var fp T + assert.Run(func(assert *test.Assert) { n, err := rand.Int(rand.Reader, fp.Modulus()) if err != nil { @@ -44,4 +46,26 @@ func testComposition[T emulated.FieldParams](t *testing.T) { assert.FailNow("unequal") } }, fmt.Sprintf("%s/limb=%d", reflect.TypeOf(fp).Name(), fp.BitsPerLimb())) + sfp, ok := any(fp).(emulated.DynamicFieldParams) + assert.True(ok, "field %T does not implement DynamicFieldParams", fp) + assert.Run(func(assert *test.Assert) { + n, err := rand.Int(rand.Reader, sfp.Modulus()) + if err != nil { + assert.FailNow("rand int", err) + } + res := make([]*big.Int, sfp.NbLimbsDynamic(babybear.Modulus())) + for i := range res { + res[i] = new(big.Int) + } + if err = limbs.Decompose(n, sfp.BitsPerLimbDynamic(babybear.Modulus()), res); err != nil { + assert.FailNow("decompose", err) + } + n2 := new(big.Int) + if err = limbs.Recompose(res, sfp.BitsPerLimbDynamic(babybear.Modulus()), n2); err != nil { + assert.FailNow("recompose", err) + } + if n2.Cmp(n) != 0 { + assert.FailNow("unequal") + } + }, fmt.Sprintf("smallfield/%s/limb=%d", reflect.TypeOf(fp).Name(), sfp.BitsPerLimbDynamic(babybear.Modulus()))) } diff --git a/std/internal/logderivarg/logderivarg.go b/std/internal/logderivarg/logderivarg.go index a5296141..40e4d41b 100644 --- a/std/internal/logderivarg/logderivarg.go +++ b/std/internal/logderivarg/logderivarg.go @@ -45,7 +45,7 @@ import ( "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/std/hash/mimc" + "github.com/consensys/gnark/std/internal/mimc" "github.com/consensys/gnark/std/multicommit" ) @@ -148,6 +148,10 @@ func Build(api frontend.API, table Table, queries Table) error { } func randLinearCoefficients(api frontend.API, nbRow int, commitment frontend.Variable) (rowCoeffs []frontend.Variable, challenge frontend.Variable) { + if nbRow == 1 { + // to avoid initializing the hasher. + return []frontend.Variable{1}, commitment + } hasher, err := mimc.NewMiMC(api) if err != nil { panic(err) diff --git a/std/hash/mimc/encrypt.go b/std/internal/mimc/encrypt.go similarity index 100% rename from std/hash/mimc/encrypt.go rename to std/internal/mimc/encrypt.go diff --git a/std/internal/mimc/mimc.go b/std/internal/mimc/mimc.go new file mode 100644 index 00000000..3563a749 --- /dev/null +++ b/std/internal/mimc/mimc.go @@ -0,0 +1,97 @@ +// Copyright 2020-2025 Consensys Software Inc. +// Licensed under the Apache License, Version 2.0. See the LICENSE file for details. + +// Package mimc implements the MiMC hash function as a gnark circuit +// +// This is an internal package as the implementation is used for some internal +// components and proof recursion where importing the +// [github.com/consensys/gnark/std/hash] package would create an import cycle. +package mimc + +import ( + "errors" + "math/big" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/utils" +) + +// MiMC contains the params of the MiMC hash func and the curves on which it is implemented. +// +// NB! See the package documentation for length extension attack consideration. +type MiMC struct { + params []big.Int // slice containing constants for the encryption rounds + id ecc.ID // id needed to know which encryption function to use + h frontend.Variable // current vector in the Miyaguchi–Preneel scheme + data []frontend.Variable // state storage. data is updated when Write() is called. Sum sums the data. + api frontend.API // underlying constraint system +} + +// NewMiMC returns a MiMC instance that can be used in a gnark circuit. The +// out-circuit counterpart of this function is provided in [gnark-crypto]. +// +// NB! See the package documentation for length extension attack consideration. +// +// [gnark-crypto]: https://pkg.go.dev/github.com/consensys/gnark-crypto/hash +func NewMiMC(api frontend.API) (MiMC, error) { + // TODO @gbotrel use field + if constructor, ok := newMimc[utils.FieldToCurve(api.Compiler().Field())]; ok { + return constructor(api), nil + } + return MiMC{}, errors.New("unknown curve id") +} + +// Write adds more data to the running hash. +func (h *MiMC) Write(data ...frontend.Variable) { + h.data = append(h.data, data...) +} + +// Reset resets the Hash to its initial state. +func (h *MiMC) Reset() { + h.data = nil + h.h = 0 +} + +// SetState manually sets the state of the hasher to the provided value. In the +// case of MiMC only a single frontend variable is expected to represent the +// state. +func (h *MiMC) SetState(newState []frontend.Variable) error { + + if len(h.data) > 0 { + return errors.New("the hasher is not in an initial state") + } + + if len(newState) != 1 { + return errors.New("the MiMC hasher expects a single field element to represent the state") + } + + h.h = newState[0] + h.data = nil + return nil +} + +// State returns the inner-state of the hasher. In the context of MiMC only a +// single field element is returned. +func (h *MiMC) State() []frontend.Variable { + h.Sum() // this flushes the unsummed data + return []frontend.Variable{h.h} +} + +// Sum hash using [Miyaguchi–Preneel] where the XOR operation is replaced by +// field addition. +// +// [Miyaguchi–Preneel]: https://en.wikipedia.org/wiki/One-way_compression_function +func (h *MiMC) Sum() frontend.Variable { + + //h.Write(data...)s + for _, stream := range h.data { + r := encryptFuncs[h.id](*h, stream) + h.h = h.api.Add(h.h, r, stream) + } + + h.data = nil // flush the data already hashed + + return h.h + +} diff --git a/std/hash/mimc/mimc_test.go b/std/internal/mimc/mimc_test.go similarity index 100% rename from std/hash/mimc/mimc_test.go rename to std/internal/mimc/mimc_test.go diff --git a/std/internal/test_vectors_utils/test_vector_utils.go b/std/internal/test_vectors_utils/test_vector_utils.go deleted file mode 100644 index 80927660..00000000 --- a/std/internal/test_vectors_utils/test_vector_utils.go +++ /dev/null @@ -1,260 +0,0 @@ -package test_vector_utils - -import ( - "encoding/json" - "github.com/consensys/gnark/frontend" - "github.com/stretchr/testify/assert" - "os" - "path/filepath" - "strconv" - "strings" - "testing" -) - -// These data structures fail to equate different representations of the same number. i.e. 5 = -10/-2 -// @Tabaie TODO Replace with proper lookup tables - -type Map struct { - keys []frontend.Variable - values []frontend.Variable -} - -func getDelta(api frontend.API, x frontend.Variable, deltaIndex int, keys []frontend.Variable) frontend.Variable { - num := frontend.Variable(1) - den := frontend.Variable(1) - - for i, key := range keys { - if i != deltaIndex { - num = api.Mul(num, api.Sub(key, x)) - den = api.Mul(den, api.Sub(key, keys[deltaIndex])) - } - } - - return api.Div(num, den) -} - -// Get returns garbage if key is not present -func (m Map) Get(api frontend.API, key frontend.Variable) frontend.Variable { - res := frontend.Variable(0) - - for i := range m.keys { - deltaI := getDelta(api, key, i, m.keys) - res = api.MulAcc(res, deltaI, m.values[i]) - } - - return res -} - -// The keys in a DoubleMap must be constant. i.e. known at setup time -type DoubleMap struct { - keys1 []frontend.Variable - keys2 []frontend.Variable - values [][]frontend.Variable -} - -// Get is very inefficient. Do not use outside testing -func (m DoubleMap) Get(api frontend.API, key1, key2 frontend.Variable) frontend.Variable { - deltas1 := make([]frontend.Variable, len(m.keys1)) - deltas2 := make([]frontend.Variable, len(m.keys2)) - - for i := range deltas1 { - deltas1[i] = getDelta(api, key1, i, m.keys1) - } - - for j := range deltas2 { - deltas2[j] = getDelta(api, key2, j, m.keys2) - } - - res := frontend.Variable(0) - - for i := range deltas1 { - for j := range deltas2 { - if m.values[i][j] != nil { - deltaIJ := api.Mul(deltas1[i], deltas2[j], m.values[i][j]) - res = api.Add(res, deltaIJ) - } - } - } - - return res -} - -func register[K comparable](m map[K]int, key K) { - if _, ok := m[key]; !ok { - m[key] = len(m) - } -} - -func orderKeys[K comparable](order map[K]int) (ordered []K) { - ordered = make([]K, len(order)) - for k, i := range order { - ordered[i] = k - } - return -} - -type ElementMap struct { - single Map - double DoubleMap -} - -func ReadMap(in map[string]interface{}) ElementMap { - single := Map{ - keys: make([]frontend.Variable, 0), - values: make([]frontend.Variable, 0), - } - - keys1 := make(map[string]int) - keys2 := make(map[string]int) - - for k, v := range in { - - kSep := strings.Split(k, ",") - switch len(kSep) { - case 1: - single.keys = append(single.keys, k) - single.values = append(single.values, ToVariable(v)) - case 2: - - register(keys1, kSep[0]) - register(keys2, kSep[1]) - - default: - panic("too many keys") - } - } - - vals := make([][]frontend.Variable, len(keys1)) - for i := range vals { - vals[i] = make([]frontend.Variable, len(keys2)) - } - - for k, v := range in { - kSep := strings.Split(k, ",") - if len(kSep) == 2 { - i1 := keys1[kSep[0]] - i2 := keys2[kSep[1]] - vals[i1][i2] = ToVariable(v) - } - } - - double := DoubleMap{ - keys1: ToVariableSlice(orderKeys(keys1)), - keys2: ToVariableSlice(orderKeys(keys2)), - values: vals, - } - - return ElementMap{ - single: single, - double: double, - } -} - -func ToVariable(v interface{}) frontend.Variable { - switch vT := v.(type) { - case float64: - return int(vT) - default: - return v - } -} - -func ToVariableSlice[V any](slice []V) (variableSlice []frontend.Variable) { - variableSlice = make([]frontend.Variable, len(slice)) - for i := range slice { - variableSlice[i] = ToVariable(slice[i]) - } - return -} - -func ToVariableSliceSlice[V any](sliceSlice [][]V) (variableSliceSlice [][]frontend.Variable) { - variableSliceSlice = make([][]frontend.Variable, len(sliceSlice)) - for i := range sliceSlice { - variableSliceSlice[i] = ToVariableSlice(sliceSlice[i]) - } - return -} - -func ToMap(keys1, keys2, values []frontend.Variable) map[string]interface{} { - res := make(map[string]interface{}, len(keys1)) - for i := range keys1 { - str := strconv.Itoa(keys1[i].(int)) + "," + strconv.Itoa(keys2[i].(int)) - res[str] = values[i].(int) - } - return res -} - -var MapCache = make(map[string]ElementMap) // @Tabaie: global bad? - -func ElementMapFromFile(path string) (ElementMap, error) { - path, err := filepath.Abs(path) - if err != nil { - return ElementMap{}, err - } - if h, ok := MapCache[path]; ok { - return h, nil - } - var bytes []byte - if bytes, err = os.ReadFile(path); err == nil { - var asMap map[string]interface{} - if err = json.Unmarshal(bytes, &asMap); err != nil { - return ElementMap{}, err - } - - res := ReadMap(asMap) - MapCache[path] = res - return res, nil - - } else { - return ElementMap{}, err - } -} - -type MapHash struct { - Map ElementMap - state frontend.Variable - API frontend.API - stateValid bool -} - -func (m *MapHash) Sum() frontend.Variable { - return m.state -} - -func (m *MapHash) Write(data ...frontend.Variable) { - for _, x := range data { - m.write(x) - } -} - -func (m *MapHash) Reset() { - m.stateValid = false -} - -func (m *MapHash) write(x frontend.Variable) { - if m.stateValid { - m.state = m.Map.double.Get(m.API, x, m.state) - } else { - m.state = m.Map.single.Get(m.API, x) - } - m.stateValid = true -} - -func AssertSliceEqual[T comparable](t *testing.T, expected, seen []T) { - assert.Equal(t, len(expected), len(seen)) - for i := range seen { - assert.True(t, expected[i] == seen[i], "@%d: %v != %v", i, expected[i], seen[i]) // assert.Equal is not strict enough when comparing pointers, i.e. it compares what they refer to - } -} - -func SliceEqual[T comparable](expected, seen []T) bool { - if len(expected) != len(seen) { - return false - } - for i := range seen { - if expected[i] != seen[i] { - return false - } - } - return true -} diff --git a/std/internal/test_vectors_utils/test_vector_utils_test.go b/std/internal/test_vectors_utils/test_vector_utils_test.go deleted file mode 100644 index 5049a6bd..00000000 --- a/std/internal/test_vectors_utils/test_vector_utils_test.go +++ /dev/null @@ -1,148 +0,0 @@ -package test_vector_utils - -import ( - "fmt" - "testing" - - "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/test" - "github.com/stretchr/testify/assert" -) - -type TestSingleMapCircuit struct { - M Map `gnark:"-"` - Values []frontend.Variable -} - -func (c *TestSingleMapCircuit) Define(api frontend.API) error { - - for i, k := range c.M.keys { - v := c.M.Get(api, k) - api.AssertIsEqual(v, c.Values[i]) - } - - return nil -} - -func TestSingleMap(t *testing.T) { - m := map[string]interface{}{ - "1": -2, - "4": 1, - "6": 7, - } - single := ReadMap(m).single - - assignment := TestSingleMapCircuit{ - M: single, - Values: single.values, - } - - circuit := TestSingleMapCircuit{ - M: single, - Values: make([]frontend.Variable, len(m)), // Okay to use the same object? - } - - test.NewAssert(t).CheckCircuit(&circuit, test.WithValidAssignment(&assignment)) -} - -type TestDoubleMapCircuit struct { - M DoubleMap `gnark:"-"` - Values []frontend.Variable - Keys1 []frontend.Variable `gnark:"-"` - Keys2 []frontend.Variable `gnark:"-"` -} - -func (c *TestDoubleMapCircuit) Define(api frontend.API) error { - - for i := range c.Keys1 { - v := c.M.Get(api, c.Keys1[i], c.Keys2[i]) - api.AssertIsEqual(v, c.Values[i]) - } - - return nil -} - -func TestReadDoubleMap(t *testing.T) { - keys1 := []frontend.Variable{1, 2} - keys2 := []frontend.Variable{1, 0} - values := []frontend.Variable{3, 1} - - for i := 0; i < 100; i++ { - m := ToMap(keys1, keys2, values) - double := ReadMap(m).double - valuesOrdered := [][]frontend.Variable{{3, nil}, {nil, 1}} - - assert.True(t, double.keys1[0] == "1" && double.keys1[1] == "2" || double.keys1[0] == "2" && double.keys1[1] == "1") - assert.True(t, double.keys2[0] == "1" && double.keys2[1] == "0" || double.keys2[0] == "0" && double.keys2[1] == "1") - - if double.keys1[0] != "1" { - valuesOrdered[0], valuesOrdered[1] = valuesOrdered[1], valuesOrdered[0] - } - - if double.keys2[0] != "1" { - valuesOrdered[0][0], valuesOrdered[0][1] = valuesOrdered[0][1], valuesOrdered[0][0] - valuesOrdered[1][0], valuesOrdered[1][1] = valuesOrdered[1][1], valuesOrdered[1][0] - } - - assert.True(t, slice2Eq(valuesOrdered, double.values)) - - } - -} - -func slice2Eq(s1, s2 [][]frontend.Variable) bool { - if len(s1) != len(s2) { - return false - } - for i := range s1 { - if !sliceEq(s1[i], s2[i]) { - return false - } - } - return true -} - -func sliceEq(s1, s2 []frontend.Variable) bool { - if len(s1) != len(s2) { - return false - } - for i := range s1 { - if s1[i] != s2[i] { - return false - } - } - return true -} - -func TestDoubleMap(t *testing.T) { - keys1 := []frontend.Variable{1, 5, 5, 3} - keys2 := []frontend.Variable{1, -5, 4, 4} - values := []frontend.Variable{0, 2, 3, 0} - - m := ToMap(keys1, keys2, values) - double := ReadMap(m).double - - fmt.Println(double) - - assignment := TestDoubleMapCircuit{ - M: double, - Values: values, - Keys1: keys1, - Keys2: keys2, - } - - circuit := TestDoubleMapCircuit{ - M: double, - Keys1: keys1, - Keys2: keys2, - Values: make([]frontend.Variable, len(m)), // Okay to use the same object? - } - - test.NewAssert(t).CheckCircuit(&circuit, test.WithValidAssignment(&assignment)) -} - -func TestDoubleMapManyTimes(t *testing.T) { - for i := 0; i < 100; i++ { - TestDoubleMap(t) - } -} diff --git a/std/lookup/logderivlookup/logderivlookup.go b/std/lookup/logderivlookup/logderivlookup.go index 85d3f8ea..63f2bc69 100644 --- a/std/lookup/logderivlookup/logderivlookup.go +++ b/std/lookup/logderivlookup/logderivlookup.go @@ -24,8 +24,22 @@ import ( "github.com/consensys/gnark/std/internal/logderivarg" ) -// Table holds all the entries and queries. -type Table struct { +// Table allows to insert and query values in a lookup table. +type Table interface { + // Insert inserts a new entry into the lookup table and returns its index. + // It panics if the table is already committed. + Insert(val frontend.Variable) (index int) + + // Lookup looks up values from the lookup tables given by the indices inds. It + // returns a variable for every index. It panics during compile time when + // looking up from a committed or empty table. It panics during solving time + // when the index is out of bounds. + Lookup(inds ...frontend.Variable) (vals []frontend.Variable) +} + +// table is the parametric implementation of the lookup table. For usage use +// [Table] instead to avoid referencing to the type parameter. +type table[E constraint.Element] struct { api frontend.API entries []frontend.Variable @@ -36,7 +50,7 @@ type Table struct { // the blueprint stores the lookup table entries once // such that each query only need to store the indexes to lookup bID constraint.BlueprintID - blueprint constraint.BlueprintLookupHint + blueprint constraint.BlueprintLookupHint[E] } type result struct { @@ -46,18 +60,29 @@ type result struct { // New returns a new [*Table]. It additionally defers building the // log-derivative argument. -func New(api frontend.API) *Table { - t := &Table{api: api} - api.Compiler().Defer(t.commit) +func New(api frontend.API) Table { + if constraint.FitsElement[constraint.U32](api.Compiler().Field()) { + t := &table[constraint.U32]{api: api} + api.Compiler().Defer(t.commit) + + // each table has a unique blueprint + t.bID = api.Compiler().AddBlueprint(&t.blueprint) + return t + } + if constraint.FitsElement[constraint.U64](api.Compiler().Field()) { + t := &table[constraint.U64]{api: api} + api.Compiler().Defer(t.commit) - // each table has a unique blueprint - t.bID = api.Compiler().AddBlueprint(&t.blueprint) - return t + // each table has a unique blueprint + t.bID = api.Compiler().AddBlueprint(&t.blueprint) + return t + } + panic("unsupported field type") } // Insert inserts variable val into the lookup table and returns its index as a // constant. It panics if the table is already committed. -func (t *Table) Insert(val frontend.Variable) (index int) { +func (t *table[E]) Insert(val frontend.Variable) (index int) { if t.immutable { panic("inserting into committed lookup table") } @@ -74,7 +99,7 @@ func (t *Table) Insert(val frontend.Variable) (index int) { // returns a variable for every index. It panics during compile time when // looking up from a committed or empty table. It panics during solving time // when the index is out of bounds. -func (t *Table) Lookup(inds ...frontend.Variable) (vals []frontend.Variable) { +func (t *table[E]) Lookup(inds ...frontend.Variable) (vals []frontend.Variable) { if t.immutable { panic("looking up from a committed lookup table") } @@ -89,7 +114,7 @@ func (t *Table) Lookup(inds ...frontend.Variable) (vals []frontend.Variable) { // performLookup performs the lookup and returns the resulting variables. // underneath, it does use the blueprint to encode the lookup hint. -func (t *Table) performLookup(inds []frontend.Variable) []frontend.Variable { +func (t *table[E]) performLookup(inds []frontend.Variable) []frontend.Variable { // to build the instruction, we need to first encode its dependency as a calldata []uint32 slice. // * calldata[0] is the length of the calldata, // * calldata[1] is the number of entries in the table we consider. @@ -131,7 +156,7 @@ func (t *Table) performLookup(inds []frontend.Variable) []frontend.Variable { return internalVariables } -func (t *Table) entryTable() [][]frontend.Variable { +func (t *table[E]) entryTable() [][]frontend.Variable { tbl := make([][]frontend.Variable, len(t.entries)) for i := range t.entries { tbl[i] = []frontend.Variable{i, t.entries[i]} @@ -139,7 +164,7 @@ func (t *Table) entryTable() [][]frontend.Variable { return tbl } -func (t *Table) resultsTable() [][]frontend.Variable { +func (t *table[E]) resultsTable() [][]frontend.Variable { tbl := make([][]frontend.Variable, len(t.results)) for i := range t.results { tbl[i] = []frontend.Variable{t.results[i].ind, t.results[i].val} @@ -147,6 +172,6 @@ func (t *Table) resultsTable() [][]frontend.Variable { return tbl } -func (t *Table) commit(api frontend.API) error { +func (t *table[E]) commit(api frontend.API) error { return logderivarg.Build(api, t.entryTable(), t.resultsTable()) } diff --git a/std/math/bits/conversion_binary.go b/std/math/bits/conversion_binary.go index 27b92218..eaf91819 100644 --- a/std/math/bits/conversion_binary.go +++ b/std/math/bits/conversion_binary.go @@ -1,6 +1,7 @@ package bits import ( + "fmt" "math/big" "github.com/consensys/gnark/frontend" @@ -25,6 +26,32 @@ func fromBinary(api frontend.API, digits []frontend.Variable, opts ...BaseConver panic(err) } } + // check if the inputs are all constant. In this case, recompose without adding any constraints. + allConst := true + constDigits := make([]*big.Int, len(digits)) + for i := range digits { + if constV, ok := api.Compiler().ConstantValue(digits[i]); !ok { + // there is at least one digit which is not a constant. Break out to the general case. + allConst = false + break + } else { + constDigits[len(digits)-i-1] = constV + } + } + if allConst { + res := new(big.Int) + for _, d := range constDigits { + // check that the inputs are binary digits. 1 has 1 bit and 0 has 0 bits. + if d.BitLen() > 1 { + panic(fmt.Sprintf("constant input to FromBinary has more than 1 bit. Has %d bits", d.BitLen())) + } + res.Lsh(res, 1) + res.Add(res, d) + } + res.Mod(res, api.Compiler().Field()) // ensure the result is mod reduced + return res + } + // if we are here, then we have at least one unconstrained input or the inputs are not constant. // Σbi = Σ (2**i * b[i]) Σbi := frontend.Variable(0) @@ -55,6 +82,24 @@ func toBinary(api frontend.API, v frontend.Variable, opts ...BaseConversionOptio panic(err) } } + // handle the case when the input is constant separately to avoid creating any constraints + if constV, ok := api.Compiler().ConstantValue(v); ok { + // first we ensure that the constant value is mod reduced + constV.Mod(constV, api.Compiler().Field()) + // we still want to honor the number of bits requested. And we have a + // promise that for non-constant input we would get unsatisfiable + // constraint if the bitlength of the input is larger than the option. + // For constant input, we panic instead. We can do it as it will happen + // at circuit compile time, so the developer can fix it. + if cfg.NbDigits > 0 && cfg.NbDigits < constV.BitLen() { + panic(fmt.Sprintf("constant input to ToBinary has more bits than requested by WithNbDigits option. Has %d bits, requested %d bits", constV.BitLen(), cfg.NbDigits)) + } + res := make([]frontend.Variable, cfg.NbDigits) + for i := range cfg.NbDigits { + res[i] = constV.Bit(i) + } + return res + } // by default, we also check that the value to be decomposed is less than the // modulus. However, we can omit the check when the number of bits we want diff --git a/std/math/bits/conversion_test.go b/std/math/bits/conversion_test.go index ce1d6f86..bb3285ac 100644 --- a/std/math/bits/conversion_test.go +++ b/std/math/bits/conversion_test.go @@ -1,9 +1,15 @@ package bits_test import ( + "crypto/rand" + "errors" + "fmt" + "math/big" "testing" + "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/std/math/bits" "github.com/consensys/gnark/test" ) @@ -64,3 +70,141 @@ func TestToTernary(t *testing.T) { assert := test.NewAssert(t) assert.CheckCircuit(&toTernaryCircuit{}, test.WithValidAssignment(&toTernaryCircuit{A: 5, T0: 2, T1: 1, T2: 0})) } + +type toBinaryCircuitConstantInput struct { + A frontend.Variable + constantA *big.Int + nbBits int +} + +func (c *toBinaryCircuitConstantInput) Define(api frontend.API) error { + opts := []bits.BaseConversionOption{} + if c.nbBits > 0 { + opts = append(opts, bits.WithNbDigits(c.nbBits)) + } + decomposedA := bits.ToBinary(api, c.A, opts...) + constantA := new(big.Int).Set(c.constantA) + if _, ok := api.Compiler().ConstantValue(constantA); !ok { + // we work inside a test engine. It doesn't differentiate between a constant and a variable. We manually reduce for now. + constantA.Mod(constantA, api.Compiler().Field()) + } + decomposedAConstant := bits.ToBinary(api, constantA, opts...) + if len(decomposedA) != len(decomposedAConstant) { + return errors.New("decomposedA and decomposedAConstant must have the same length") + } + for i := 0; i < len(decomposedA); i++ { + api.AssertIsEqual(decomposedA[i], decomposedAConstant[i]) + } + + return nil +} + +func TestToBinaryConstantInput(t *testing.T) { + assert := test.NewAssert(t) + + for _, v := range []int{0, 1, 2, 10, 100, 300} { + assert.Run(func(assert *test.Assert) { + val, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), uint(v))) + assert.NoError(err) + assert.CheckCircuit(&toBinaryCircuitConstantInput{constantA: val, nbBits: v}, test.WithValidAssignment(&toBinaryCircuitConstantInput{A: val})) + }, fmt.Sprintf("v=%d", v)) + } +} + +type testFromBinaryCircuitConstantInput struct { + Inputs []*big.Int + ThirdVariableBit frontend.Variable + allConstant bool + Expected frontend.Variable +} + +func (c *testFromBinaryCircuitConstantInput) Define(api frontend.API) error { + inps := make([]frontend.Variable, len(c.Inputs)) + for i, inp := range c.Inputs { + inps[i] = inp + // we also want to test the case where inside the constant inputs we have a variable + if i == 2 && !c.allConstant { + inps[i] = c.ThirdVariableBit + } + } + res := bits.FromBinary(api, inps) + api.AssertIsEqual(res, c.Expected) + api.AssertIsEqual(c.Expected, c.Expected) // dummy constraint to overcome prover bug with 1 constraint + return nil +} + +func TestFromBinaryConstantInput(t *testing.T) { + assert := test.NewAssert(t) + + for _, v := range []int{1, 2, 10, 100, 300} { + val, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), uint(v))) + assert.NoError(err) + + bts := make([]*big.Int, v) + for i := 0; i < v; i++ { + bts[i] = new(big.Int).SetUint64(uint64(val.Bit(i))) + } + assert.Run(func(assert *test.Assert) { + assert.Run(func(assert *test.Assert) { + assert.CheckCircuit(&testFromBinaryCircuitConstantInput{ + Inputs: bts, + allConstant: true, + }, + test.WithValidAssignment(&testFromBinaryCircuitConstantInput{ + ThirdVariableBit: 0, + Expected: val, + })) + }, "allconstant=true") + if v > 2 { + assert.Run(func(assert *test.Assert) { + assert.CheckCircuit(&testFromBinaryCircuitConstantInput{ + Inputs: bts, + allConstant: false, + }, + test.WithValidAssignment(&testFromBinaryCircuitConstantInput{ + Expected: val, + ThirdVariableBit: val.Bit(2), + })) + }, "allconstant=false") + } + }, fmt.Sprintf("v=%d", v)) + } +} + +type testFromBinaryInvalidInput struct { + ConstantInputs []*big.Int + VariableInputs []frontend.Variable + Variable frontend.Variable +} + +func (c *testFromBinaryInvalidInput) Define(api frontend.API) error { + if len(c.ConstantInputs) != 0 { + inps := make([]frontend.Variable, len(c.ConstantInputs)) + for i, inp := range c.ConstantInputs { + inps[i] = inp + } + // test when constant inputs are not binary + res := bits.FromBinary(api, inps) + api.AssertIsDifferent(res, 0) + // ensure we have at least two constraints to overcome PLONK prover bug with 1 constraint only + api.AssertIsEqual(c.Variable, c.Variable) + api.AssertIsEqual(c.Variable, c.Variable) + } else { + res := bits.FromBinary(api, c.VariableInputs) + api.AssertIsDifferent(res, 0) + } + return nil +} + +func TestFromBinaryInvalidInput(t *testing.T) { + assert := test.NewAssert(t) + + _, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &testFromBinaryInvalidInput{ + ConstantInputs: []*big.Int{big.NewInt(2), big.NewInt(1)}, + }) + assert.Error(err) + assert.CheckCircuit(&testFromBinaryInvalidInput{VariableInputs: make([]frontend.Variable, 2)}, test.WithInvalidAssignment(&testFromBinaryInvalidInput{ + VariableInputs: []frontend.Variable{2, 1}, + Variable: big.NewInt(3), + })) +} diff --git a/std/math/cmp/bounded.go b/std/math/cmp/bounded.go index 14d5a433..9954d68e 100644 --- a/std/math/cmp/bounded.go +++ b/std/math/cmp/bounded.go @@ -2,10 +2,11 @@ package cmp import ( "fmt" + "math/big" + "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/std/math/bits" - "math/big" ) func init() { diff --git a/std/math/cmp/bounded_test.go b/std/math/cmp/bounded_test.go index fcdfc067..7e6e2baa 100644 --- a/std/math/cmp/bounded_test.go +++ b/std/math/cmp/bounded_test.go @@ -1,11 +1,12 @@ package cmp_test import ( + "math/big" + "testing" + "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/std/math/cmp" "github.com/consensys/gnark/test" - "math/big" - "testing" ) func TestAssertIsLessEq(t *testing.T) { diff --git a/std/math/cmp/generic.go b/std/math/cmp/generic.go index 016e0387..b5f4b2bb 100644 --- a/std/math/cmp/generic.go +++ b/std/math/cmp/generic.go @@ -2,9 +2,10 @@ package cmp import ( + "math/big" + "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/std/math/bits" - "math/big" ) // IsEqual returns 1 if a = b, and returns 0 if a != b. a and b should be diff --git a/std/math/cmp/generic_test.go b/std/math/cmp/generic_test.go index 2f1a8e3e..12c3885e 100644 --- a/std/math/cmp/generic_test.go +++ b/std/math/cmp/generic_test.go @@ -1,12 +1,13 @@ package cmp import ( + "math/big" + "testing" + "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/std/math/bits" "github.com/consensys/gnark/test" - "math/big" - "testing" ) type isLessRecursive4BitCircuit struct { diff --git a/std/math/emulated/element.go b/std/math/emulated/element.go index 2603deb6..dd3f2649 100644 --- a/std/math/emulated/element.go +++ b/std/math/emulated/element.go @@ -41,6 +41,16 @@ type Element[T FieldParams] struct { isEvaluated bool evaluation frontend.Variable `gnark:"-"` + + // witnessValue stores the value of the witness. We set Limbs from it when + // calling the [Element.Initialize] method. + // + // NB! Even though we have documented not to use [ValueOf] method inside + // a circuit to define constants, then many users still do it. In that case, + // the [Element.Initialize] method is not called during witness parsing time and + // we need to do it before using the limbs. This is automatically done + // in [Field.enforceWidthConditional] method. + witnessValue *big.Int } // ValueOf returns an Element[T] from a constant value. This method is used for @@ -48,27 +58,25 @@ type Element[T FieldParams] struct { // [Field.NewElement] method. // // The input is converted into limbs according to the parameters of the field -// and returned as a new [Element[T]]. Note that it returns the value, not a +// and returned as a new [Element]. Note that it returns the value, not a // reference, which is more convenient for witness assignment. +// +// The method is asynchronous and the limb decomposition is done during witness +// parsing. func ValueOf[T FieldParams](constant interface{}) Element[T] { - // in this method we set the isWitness flag to true, because we do not know - // the width of the input value. Even though it is valid to call this method - // in circuit without reference to `Field`, then the canonical way would be - // to call [Field.NewElement] method (which would set isWitness to false). - if constant == nil { - r := newConstElement[T](0, true) - return *r + bValue := utils.FromInterface(constant) + return Element[T]{ + witnessValue: &bValue, } - r := newConstElement[T](constant, true) - return *r } // newConstElement is shorthand for initialising new element using NewElement and // taking pointer to it. We only want to have a public method for initialising // an element which return a value because the user uses this only for witness // creation and it mess up schema parsing. -func newConstElement[T FieldParams](v interface{}, isWitness bool) *Element[T] { +func newConstElement[T FieldParams](field *big.Int, v interface{}, isWitness bool) *Element[T] { var fp T + effNbLimbs, effNbBits := GetEffectiveFieldParams[T](field) // convert to big.Int bValue := utils.FromInterface(v) @@ -83,16 +91,16 @@ func newConstElement[T FieldParams](v interface{}, isWitness bool) *Element[T] { // constant), thus we can allocate the exact number of limbs. var nbLimbs int if isWitness { - nbLimbs = int(fp.NbLimbs()) + nbLimbs = int(effNbLimbs) } else { - nbLimbs = (bValue.BitLen() + int(fp.BitsPerLimb()) - 1) / int(fp.BitsPerLimb()) + nbLimbs = (bValue.BitLen() + int(effNbBits) - 1) / int(effNbBits) } // TODO @gbotrel use big.Int pool here blimbs := make([]*big.Int, nbLimbs) for i := range blimbs { blimbs[i] = new(big.Int) } - if err := limbs.Decompose(&bValue, fp.BitsPerLimb(), blimbs); err != nil { + if err := limbs.Decompose(&bValue, effNbBits, blimbs); err != nil { panic(fmt.Errorf("decompose value: %w", err)) } @@ -114,10 +122,25 @@ func (f *Field[T]) newInternalElement(limbs []frontend.Variable, overflow uint) return &Element[T]{Limbs: limbs, overflow: overflow, internal: true} } -// GnarkInitHook describes how to initialise the element. -func (e *Element[T]) GnarkInitHook() { +// Initialize automatically initializes non-native element during circuit parsing and compilation. +// It allocates the limbs and sets the element to be automatically range-checked on first use. +// +// The method has a side effect that when a circuit is parsed multiple times, then the subsequent +// calls to this method will not re-initialize the element. Thus any changes to the non-native element +// persist. +func (e *Element[T]) Initialize(field *big.Int) { + if e == nil { + return // we cannot initialize nil element + } + if e.Limbs == nil && field == nil { + panic("field is nil") + } if e.Limbs == nil { - *e = ValueOf[T](0) + if e.witnessValue == nil { + *e = *newConstElement[T](field, 0, true) + } else { + *e = *newConstElement[T](field, e.witnessValue, true) + } e.internal = false // we need to constrain in later. } // set modReduced to false - in case the circuit is compiled we may change @@ -135,5 +158,35 @@ func (e *Element[T]) copy() *Element[T] { r.overflow = e.overflow r.internal = e.internal r.modReduced = e.modReduced + r.isEvaluated = e.isEvaluated + r.evaluation = e.evaluation + if e.witnessValue != nil { + r.witnessValue = new(big.Int).Set(e.witnessValue) + } return &r } + +// isStrictZero checks if the element is strictly zero by convention. Can be +// used for determining if to take fast paths. +func (e *Element[T]) isStrictZero() bool { + if e == nil { + // conventionally we could say it is zero, but this can lead to some strange + // edge cases where use uninitialized elements. So we just panic. + panic("nil element. Uninitialized element?") + } + switch { + case e.Limbs == nil && e.witnessValue == nil: + // here also we could conventionally say it is zero, but this case usually + // means we use uninitialized element. + panic("nil limbs and witness value. Uninitialized element?") + case e.Limbs == nil && e.witnessValue != nil: + return e.witnessValue.Sign() == 0 + case e.Limbs != nil && len(e.Limbs) == 0: + // by convention we say that empty limbs are zero + return true + default: + // we could potentially check that the limbs are all zero (or multiple of the modulus), + // but for consistency we just return false and take potential performance hit. + return false + } +} diff --git a/std/math/emulated/element_test.go b/std/math/emulated/element_test.go index fa8ebaa0..2cee8932 100644 --- a/std/math/emulated/element_test.go +++ b/std/math/emulated/element_test.go @@ -890,6 +890,7 @@ func testAssertIsInRange[T FieldParams](t *testing.T) { witness := AssertInRangeCircuit[T]{X: ValueOf[T](X)} assert.CheckCircuit(&circuit, test.WithValidAssignment(&witness)) witness2 := AssertInRangeCircuit[T]{X: ValueOf[T](0)} + witness2.X.Limbs = make([]frontend.Variable, fp.NbLimbs()) t := 0 for i := 0; i < int(fp.NbLimbs())-1; i++ { L := new(big.Int).Lsh(big.NewInt(1), fp.BitsPerLimb()) @@ -1500,3 +1501,94 @@ func testFastPaths[T FieldParams](t *testing.T) { assert.CheckCircuit(circuit, test.WithValidAssignment(assignment)) } + +type TestAssertIsDifferentCircuit[T FieldParams] struct { + A, B Element[T] + addMod bool +} + +func (c *TestAssertIsDifferentCircuit[T]) Define(api frontend.API) error { + f, err := NewField[T](api) + if err != nil { + return err + } + b := &c.B + if c.addMod { + b = f.Add(b, f.Modulus()) + } + f.AssertIsDifferent(&c.A, b) + return nil +} + +func TestAssertIsDifferent(t *testing.T) { + testAssertIsDifferent[Goldilocks](t) + testAssertIsDifferent[Secp256k1Fp](t) + testAssertIsDifferent[BN254Fp](t) +} + +func testAssertIsDifferent[T FieldParams](t *testing.T) { + assert := test.NewAssert(t) + circuitNoMod := &TestAssertIsDifferentCircuit[T]{addMod: false} + var fp T + a, _ := rand.Int(rand.Reader, fp.Modulus()) + assignment1 := &TestAssertIsDifferentCircuit[T]{A: ValueOf[T](a), B: ValueOf[T](a)} + var b *big.Int + for { + b, _ = rand.Int(rand.Reader, fp.Modulus()) + if b.Cmp(a) == 0 { + continue + } + break + } + assignment2 := &TestAssertIsDifferentCircuit[T]{A: ValueOf[T](a), B: ValueOf[T](b)} + assert.CheckCircuit(circuitNoMod, test.WithInvalidAssignment(assignment1), test.WithValidAssignment(assignment2)) + + circuitWithMod := &TestAssertIsDifferentCircuit[T]{addMod: true} + assignment3 := &TestAssertIsDifferentCircuit[T]{A: ValueOf[T](a), B: ValueOf[T](a)} + assignment4 := &TestAssertIsDifferentCircuit[T]{A: ValueOf[T](a), B: ValueOf[T](b)} + assert.CheckCircuit(circuitWithMod, test.WithInvalidAssignment(assignment3), test.WithValidAssignment(assignment4)) +} + +type TestLookup2AndMuxOnAllLimbsCircuit[T FieldParams] struct { + A Element[T] `gnark:",public"` +} + +func (c *TestLookup2AndMuxOnAllLimbsCircuit[T]) Define(api frontend.API) error { + f, err := NewField[T](api) + if err != nil { + return err + } + + one := f.One() + res := f.Lookup2(1, 0, one, &c.A, &c.A, &c.A) + if len(res.Limbs) != len(c.A.Limbs) { + return fmt.Errorf("unexpected number of limbs: got %d, expected %d", len(res.Limbs), len(c.A.Limbs)) + } + for i := range res.Limbs { + api.AssertIsEqual(res.Limbs[i], c.A.Limbs[i]) + } + + res2 := f.Mux(1, one, &c.A, &c.A, &c.A) + if len(res2.Limbs) != len(c.A.Limbs) { + return fmt.Errorf("unexpected number of limbs: got %d, expected %d", len(res2.Limbs), len(c.A.Limbs)) + } + for i := range res2.Limbs { + api.AssertIsEqual(res2.Limbs[i], c.A.Limbs[i]) + } + return nil +} + +// TestLookup2AndMuxOnAllLimbs tests the Lookup2 and Mux switch all limbs. +func TestLookup2AndMuxOnAllLimbs(t *testing.T) { + testLookup2AndMuxOnAllLimbs[Goldilocks](t) + testLookup2AndMuxOnAllLimbs[Secp256k1Fp](t) + testLookup2AndMuxOnAllLimbs[BN254Fp](t) +} + +func testLookup2AndMuxOnAllLimbs[T FieldParams](t *testing.T) { + assert := test.NewAssert(t) + var fp T + a, _ := rand.Int(rand.Reader, fp.Modulus()) + assignment := &TestLookup2AndMuxOnAllLimbsCircuit[T]{A: ValueOf[T](a)} + assert.CheckCircuit(&TestLookup2AndMuxOnAllLimbsCircuit[T]{}, test.WithValidAssignment(assignment)) +} diff --git a/std/math/emulated/emparams/emparams.go b/std/math/emulated/emparams/emparams.go index 51e3ed8c..4ea8d15c 100644 --- a/std/math/emulated/emparams/emparams.go +++ b/std/math/emulated/emparams/emparams.go @@ -9,6 +9,23 @@ // func (SmallField) BitsPerLimb() uint { return 11 } // func (SmallField) IsPrime() bool { return true } // func (SmallField) Modulus() *big.Int { return big.NewInt(1032) } +// +// If in addition the parameters should be aware of the underlying native field, +// then the type should implement [DynamicFieldParams] interface. For example, +// by adding the methods: +// +// func (SmallField) NbLimbsDynamic(field *big.Int) uint { +// if smallfields.IsSmallField(field) { +// return 2 +// } +// return 1 +// } +// func (SmallField) BitsPerLimbDynamic(field *big.Int) uint { +// if smallfields.IsSmallField(field) { +// return 6 +// } +// return 11 +// } package emparams import ( @@ -17,6 +34,7 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/field/goldilocks" + "github.com/consensys/gnark/internal/smallfields" ) type fourLimbPrimeField struct{} @@ -24,29 +42,83 @@ type fourLimbPrimeField struct{} func (fourLimbPrimeField) NbLimbs() uint { return 4 } func (fourLimbPrimeField) BitsPerLimb() uint { return 64 } func (fourLimbPrimeField) IsPrime() bool { return true } +func (f fourLimbPrimeField) NbLimbsDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 26 + } + return f.NbLimbs() +} +func (f fourLimbPrimeField) BitsPerLimbDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 10 + } + return f.BitsPerLimb() +} type fiveLimbPrimeField struct{} func (fiveLimbPrimeField) NbLimbs() uint { return 5 } func (fiveLimbPrimeField) BitsPerLimb() uint { return 64 } func (fiveLimbPrimeField) IsPrime() bool { return true } +func (f fiveLimbPrimeField) NbLimbsDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 20 + } + return f.NbLimbs() +} +func (f fiveLimbPrimeField) BitsPerLimbDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 16 + } + return f.BitsPerLimb() +} type sixLimbPrimeField struct{} func (sixLimbPrimeField) NbLimbs() uint { return 6 } func (sixLimbPrimeField) BitsPerLimb() uint { return 64 } func (sixLimbPrimeField) IsPrime() bool { return true } +func (f sixLimbPrimeField) NbLimbsDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 24 + } + return f.NbLimbs() +} +func (f sixLimbPrimeField) BitsPerLimbDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 16 + } + return f.BitsPerLimb() +} type twelveLimbPrimeField struct{} func (twelveLimbPrimeField) NbLimbs() uint { return 12 } func (twelveLimbPrimeField) BitsPerLimb() uint { return 64 } func (twelveLimbPrimeField) IsPrime() bool { return true } +func (f twelveLimbPrimeField) NbLimbsDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 48 + } + return f.NbLimbs() +} +func (f twelveLimbPrimeField) BitsPerLimbDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 16 + } + return f.BitsPerLimb() +} type oneLimbPrimeField struct{} func (oneLimbPrimeField) NbLimbs() uint { return 1 } func (oneLimbPrimeField) IsPrime() bool { return true } +func (f oneLimbPrimeField) NbLimbsDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 4 + } + return f.NbLimbs() +} // Goldilocks provides type parametrization for field emulation: // - limbs: 1 @@ -58,8 +130,14 @@ func (oneLimbPrimeField) IsPrime() bool { return true } // 18446744069414584321 (base 10) type Goldilocks struct{ oneLimbPrimeField } -func (fp Goldilocks) BitsPerLimb() uint { return 64 } -func (fp Goldilocks) Modulus() *big.Int { return goldilocks.Modulus() } +func (Goldilocks) BitsPerLimb() uint { return 64 } +func (Goldilocks) Modulus() *big.Int { return goldilocks.Modulus() } +func (Goldilocks) BitsPerLimbDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 16 + } + return 64 +} // Secp256k1Fp provides type parametrization for field emulation: // - limbs: 4 @@ -73,7 +151,7 @@ func (fp Goldilocks) Modulus() *big.Int { return goldilocks.Modulus() } // This is the base field of the SECP256k1 curve. type Secp256k1Fp struct{ fourLimbPrimeField } -func (fp Secp256k1Fp) Modulus() *big.Int { return ecc.SECP256K1.BaseField() } +func (Secp256k1Fp) Modulus() *big.Int { return ecc.SECP256K1.BaseField() } // Secp256k1Fr provides type parametrization for field emulation: // - limbs: 4 @@ -87,7 +165,7 @@ func (fp Secp256k1Fp) Modulus() *big.Int { return ecc.SECP256K1.BaseField() } // This is the scalar field of the SECP256k1 curve. type Secp256k1Fr struct{ fourLimbPrimeField } -func (fp Secp256k1Fr) Modulus() *big.Int { return ecc.SECP256K1.ScalarField() } +func (Secp256k1Fr) Modulus() *big.Int { return ecc.SECP256K1.ScalarField() } // BN254Fp provides type parametrization for field emulation: // - limbs: 4 @@ -101,7 +179,7 @@ func (fp Secp256k1Fr) Modulus() *big.Int { return ecc.SECP256K1.ScalarField() } // This is the base field of the BN254 curve. type BN254Fp struct{ fourLimbPrimeField } -func (fp BN254Fp) Modulus() *big.Int { return ecc.BN254.BaseField() } +func (BN254Fp) Modulus() *big.Int { return ecc.BN254.BaseField() } // BN254Fr provides type parametrization for field emulation: // - limbs: 4 @@ -115,7 +193,7 @@ func (fp BN254Fp) Modulus() *big.Int { return ecc.BN254.BaseField() } // This is the scalar field of the BN254 curve. type BN254Fr struct{ fourLimbPrimeField } -func (fp BN254Fr) Modulus() *big.Int { return ecc.BN254.ScalarField() } +func (BN254Fr) Modulus() *big.Int { return ecc.BN254.ScalarField() } // BLS12377Fp provides type parametrization for field emulation: // - limbs: 6 @@ -129,7 +207,7 @@ func (fp BN254Fr) Modulus() *big.Int { return ecc.BN254.ScalarField() } // This is the base field of the BLS12-377 curve. type BLS12377Fp struct{ sixLimbPrimeField } -func (fp BLS12377Fp) Modulus() *big.Int { return ecc.BLS12_377.BaseField() } +func (BLS12377Fp) Modulus() *big.Int { return ecc.BLS12_377.BaseField() } // BLS12377Fr provides type parametrization for field emulation: // - limbs: 4 @@ -143,7 +221,7 @@ func (fp BLS12377Fp) Modulus() *big.Int { return ecc.BLS12_377.BaseField() } // This is the scalar field of the BLS12-377 curve. type BLS12377Fr struct{ fourLimbPrimeField } -func (fr BLS12377Fr) Modulus() *big.Int { return ecc.BLS12_377.ScalarField() } +func (BLS12377Fr) Modulus() *big.Int { return ecc.BLS12_377.ScalarField() } // BLS12381Fp provides type parametrization for field emulation: // - limbs: 6 @@ -157,7 +235,7 @@ func (fr BLS12377Fr) Modulus() *big.Int { return ecc.BLS12_377.ScalarField() } // This is the base field of the BLS12-381 curve. type BLS12381Fp struct{ sixLimbPrimeField } -func (fp BLS12381Fp) Modulus() *big.Int { return ecc.BLS12_381.BaseField() } +func (BLS12381Fp) Modulus() *big.Int { return ecc.BLS12_381.BaseField() } // BLS12381Fr provides type parametrization for field emulation: // - limbs: 4 @@ -171,7 +249,7 @@ func (fp BLS12381Fp) Modulus() *big.Int { return ecc.BLS12_381.BaseField() } // This is the scalar field of the BLS12-381 curve. type BLS12381Fr struct{ fourLimbPrimeField } -func (fp BLS12381Fr) Modulus() *big.Int { return ecc.BLS12_381.ScalarField() } +func (BLS12381Fr) Modulus() *big.Int { return ecc.BLS12_381.ScalarField() } // P256Fp provides type parametrization for field emulation: // - limbs: 4 @@ -241,7 +319,7 @@ func (P384Fr) Modulus() *big.Int { return elliptic.P384().Params().N } // This is the base field of the BW6-761 curve. type BW6761Fp struct{ twelveLimbPrimeField } -func (fp BW6761Fp) Modulus() *big.Int { return ecc.BW6_761.BaseField() } +func (BW6761Fp) Modulus() *big.Int { return ecc.BW6_761.BaseField() } // BW6761Fr provides type parametrization for field emulation: // - limbs: 6 @@ -255,7 +333,7 @@ func (fp BW6761Fp) Modulus() *big.Int { return ecc.BW6_761.BaseField() } // This is the scalar field of the BW6-761 curve. type BW6761Fr struct{ sixLimbPrimeField } -func (fp BW6761Fr) Modulus() *big.Int { return ecc.BW6_761.ScalarField() } +func (BW6761Fr) Modulus() *big.Int { return ecc.BW6_761.ScalarField() } // BLS24315Fp provides type parametrization for field emulation: // - limbs: 5 @@ -269,7 +347,7 @@ func (fp BW6761Fr) Modulus() *big.Int { return ecc.BW6_761.ScalarField() } // This is the base field of the BLS24-315 curve. type BLS24315Fp struct{ fiveLimbPrimeField } -func (fp BLS24315Fp) Modulus() *big.Int { return ecc.BLS24_315.BaseField() } +func (BLS24315Fp) Modulus() *big.Int { return ecc.BLS24_315.BaseField() } // BLS24315Fr provides type parametrization for field emulation: // - limbs: 4 @@ -283,7 +361,7 @@ func (fp BLS24315Fp) Modulus() *big.Int { return ecc.BLS24_315.BaseField() } // This is the scalar field of the BLS24-315 curve. type BLS24315Fr struct{ fourLimbPrimeField } -func (fr BLS24315Fr) Modulus() *big.Int { return ecc.BLS24_315.ScalarField() } +func (BLS24315Fr) Modulus() *big.Int { return ecc.BLS24_315.ScalarField() } // STARKCurveFp provides type parametrization for field emulation: // - limbs: 4 @@ -297,7 +375,7 @@ func (fr BLS24315Fr) Modulus() *big.Int { return ecc.BLS24_315.ScalarField() } // This is the base field of the STARK curve. type STARKCurveFp struct{ fourLimbPrimeField } -func (fp STARKCurveFp) Modulus() *big.Int { return ecc.STARK_CURVE.BaseField() } +func (STARKCurveFp) Modulus() *big.Int { return ecc.STARK_CURVE.BaseField() } // STARKCurveFr provides type parametrization for field emulation: // - limbs: 4 @@ -311,7 +389,7 @@ func (fp STARKCurveFp) Modulus() *big.Int { return ecc.STARK_CURVE.BaseField() } // This is the scalar field of the STARK curve. type STARKCurveFr struct{ fourLimbPrimeField } -func (fp STARKCurveFr) Modulus() *big.Int { return ecc.STARK_CURVE.ScalarField() } +func (STARKCurveFr) Modulus() *big.Int { return ecc.STARK_CURVE.ScalarField() } // Mod1e4096 provides type parametrization for emulated arithmetic: // - limbs: 64 @@ -331,6 +409,18 @@ func (Mod1e4096) Modulus() *big.Int { val, _ := new(big.Int).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) return val } +func (f Mod1e4096) NbLimbsDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 256 + } + return f.NbLimbs() +} +func (f Mod1e4096) BitsPerLimbDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 16 + } + return f.BitsPerLimb() +} // Mod1e512 provides type parametrization for emulated arithmetic: // - limbs: 8 @@ -350,6 +440,18 @@ func (Mod1e512) Modulus() *big.Int { val, _ := new(big.Int).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) return val } +func (f Mod1e512) NbLimbsDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 32 + } + return f.NbLimbs() +} +func (f Mod1e512) BitsPerLimbDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 16 + } + return f.BitsPerLimb() +} // Mod1e256 provides type parametrization for emulated arithmetic: // - limbs: 4 @@ -369,6 +471,18 @@ func (Mod1e256) Modulus() *big.Int { val, _ := new(big.Int).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) return val } +func (f Mod1e256) NbLimbsDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 16 + } + return f.NbLimbs() +} +func (f Mod1e256) BitsPerLimbDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 16 + } + return f.BitsPerLimb() +} // BabyBear provides type parametrization for field emulation: // - limbs: 1 @@ -385,6 +499,18 @@ type BabyBear struct{ oneLimbPrimeField } func (BabyBear) BitsPerLimb() uint { return 31 } func (BabyBear) Modulus() *big.Int { return big.NewInt(2013265921) } +func (f BabyBear) NbLimbsDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 2 + } + return f.NbLimbs() +} +func (f BabyBear) BitsPerLimbDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 16 + } + return f.BitsPerLimb() +} // KoalaBear provides type parametrization for field emulation: // - limbs: 1 @@ -401,3 +527,15 @@ type KoalaBear struct{ oneLimbPrimeField } func (KoalaBear) BitsPerLimb() uint { return 31 } func (KoalaBear) Modulus() *big.Int { return big.NewInt(2130706433) } +func (f KoalaBear) NbLimbsDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 2 + } + return f.NbLimbs() +} +func (f KoalaBear) BitsPerLimbDynamic(field *big.Int) uint { + if smallfields.IsSmallField(field) { + return 16 + } + return f.BitsPerLimb() +} diff --git a/std/math/emulated/field.go b/std/math/emulated/field.go index 7dca5383..5bbf26f6 100644 --- a/std/math/emulated/field.go +++ b/std/math/emulated/field.go @@ -8,9 +8,11 @@ import ( "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/internal/kvstore" + "github.com/consensys/gnark/internal/smallfields" "github.com/consensys/gnark/internal/utils" "github.com/consensys/gnark/logger" limbs "github.com/consensys/gnark/std/internal/limbcomposition" + "github.com/consensys/gnark/std/math/fieldextension" "github.com/consensys/gnark/std/rangecheck" "github.com/rs/zerolog" "golang.org/x/exp/constraints" @@ -23,25 +25,25 @@ import ( type Field[T FieldParams] struct { // api is the native API api frontend.API + // extensionApi is the extension API when we need to perform multiplication checks over the extension field + extensionApi fieldextension.Field - // f carries the ring parameters - fParams T + // fParams carries the ring parameters + fParams staticFieldParams[T] // maxOf is the maximum overflow before the element must be reduced. maxOf uint maxOfOnce sync.Once // constants for often used elements n, 0 and 1. Allocated only once - nConstOnce sync.Once - nConst *Element[T] - nprevConstOnce sync.Once - nprevConst *Element[T] - zeroConstOnce sync.Once - zeroConst *Element[T] - oneConstOnce sync.Once - oneConst *Element[T] - shortOneConstOnce sync.Once - shortOneConst *Element[T] + nConstOnce sync.Once + nConst *Element[T] + nprevConstOnce sync.Once + nprevConst *Element[T] + zeroConstOnce sync.Once + zeroConst *Element[T] + oneConstOnce sync.Once + oneConst *Element[T] log zerolog.Logger @@ -56,9 +58,6 @@ type ctxKey[T FieldParams] struct{} // NewField returns an object to be used in-circuit to perform emulated // arithmetic over the field defined by type parameter [FieldParams]. The // operations on this type are defined on [Element]. -// -// This is an experimental feature and performing emulated arithmetic in-circuit -// is extremely costly. See package doc for more info. func NewField[T FieldParams](native frontend.API) (*Field[T], error) { if storer, ok := native.(kvstore.Store); ok { ff := storer.GetKeyValue(ctxKey[T]{}) @@ -71,6 +70,15 @@ func NewField[T FieldParams](native frontend.API) (*Field[T], error) { log: logger.Logger(), constrainedLimbs: make(map[[16]byte]struct{}), checker: rangecheck.New(native), + fParams: newStaticFieldParams[T](native.Compiler().Field()), + } + if smallfields.IsSmallField(native.Compiler().Field()) { + f.log.Debug().Msg("using small native field, multiplication checks will be performed in extension field") + extapi, err := fieldextension.NewExtension(native) + if err != nil { + return nil, fmt.Errorf("extension field: %w", err) + } + f.extensionApi = extapi } // ensure prime is correctly set @@ -131,7 +139,7 @@ func (f *Field[T]) NewElement(v interface{}) *Element[T] { // the input was not a variable, so it must be a constant. Create a new // element from it while setting isWitness flag to false. This ensures that // we use the minimal number of limbs necessary. - c := newConstElement[T](v, false) + c := newConstElement[T](f.api.Compiler().Field(), v, false) return c } @@ -154,7 +162,7 @@ func (f *Field[T]) One() *Element[T] { // Modulus returns the modulus of the emulated ring as a constant. func (f *Field[T]) Modulus() *Element[T] { f.nConstOnce.Do(func() { - f.nConst = newConstElement[T](f.fParams.Modulus(), false) + f.nConst = newConstElement[T](f.api.Compiler().Field(), f.fParams.Modulus(), false) }) return f.nConst } @@ -162,7 +170,7 @@ func (f *Field[T]) Modulus() *Element[T] { // modulusPrev returns modulus-1 as a constant. func (f *Field[T]) modulusPrev() *Element[T] { f.nprevConstOnce.Do(func() { - f.nprevConst = newConstElement[T](new(big.Int).Sub(f.fParams.Modulus(), big.NewInt(1)), false) + f.nprevConst = newConstElement[T](f.api.Compiler().Field(), new(big.Int).Sub(f.fParams.Modulus(), big.NewInt(1)), false) }) return f.nprevConst } @@ -183,6 +191,11 @@ func (f *Field[T]) enforceWidthConditional(a *Element[T]) (didConstrain bool) { // for some reason called on nil return false } + // ensure that when the element is defined in-circuit with [ValueOf] method + // (as a constant), then we decompose it into limbs. When [ValueOf] is called + // for a witness assignment, then [Element.Initialize] is already called at + // witness parsing time. In that case, the below operation is no-op. + a.Initialize(f.api.Compiler().Field()) if a.internal { // internal elements are already constrained in the method which returned it return false @@ -235,6 +248,15 @@ func (f *Field[T]) enforceWidthConditional(a *Element[T]) (didConstrain bool) { } func (f *Field[T]) constantValue(v *Element[T]) (*big.Int, bool) { + // this case happens when we have called [ValueOf] inside a circuit as + // [Element.Initialize] has not been called (Limbs are nil). In this case, + // we can directly use the witness value as the constant value. + if v.Limbs == nil && v.witnessValue != nil { + return new(big.Int).Set(v.witnessValue), true + } + + // otherwise - it may happen that the user has manually constructed [Element] from constant limbs. + // In this case, we can recompose the constant value from the limbs. var ok bool constLimbs := make([]*big.Int, len(v.Limbs)) diff --git a/std/math/emulated/field_assert.go b/std/math/emulated/field_assert.go index e28f7c7d..211adea3 100644 --- a/std/math/emulated/field_assert.go +++ b/std/math/emulated/field_assert.go @@ -104,7 +104,7 @@ func (f *Field[T]) AssertIsInRange(a *Element[T]) { // the modulus. func (f *Field[T]) IsZero(a *Element[T]) frontend.Variable { // fast path - when the element is on zero limbs, then it is always zero - if len(a.Limbs) == 0 { + if a.isStrictZero() { return 1 } @@ -153,6 +153,14 @@ func (f *Field[T]) IsZero(a *Element[T]) frontend.Variable { return f.api.Or(res0, resP) } +// AssertIsDifferent asserts that a and b are different. +func (f *Field[T]) AssertIsDifferent(a, b *Element[T]) { + // we skip conditional width checking as it is done in [Sub] below + diff := f.Sub(a, b) + diffIsZero := f.IsZero(diff) + f.api.AssertIsEqual(diffIsZero, 0) +} + // // Cmp returns: // // - -1 if a < b // // - 0 if a = b @@ -172,18 +180,3 @@ func (f *Field[T]) IsZero(a *Element[T]) frontend.Variable { // } // return res // } - -// TODO(@ivokub) -// func (f *Field[T]) AssertIsDifferent(a, b *Element[T]) { -// ca := f.Reduce(a) -// f.AssertIsInRange(ca) -// cb := f.Reduce(b) -// f.AssertIsInRange(cb) -// var res frontend.Variable = 0 -// for i := 0; i < int(f.fParams.NbLimbs()); i++ { -// cmp := f.api.Cmp(ca.Limbs[i], cb.Limbs[i]) -// cmpsq := f.api.Mul(cmp, cmp) -// res = f.api.Add(res, cmpsq) -// } -// f.api.AssertIsDifferent(res, 0) -// } diff --git a/std/math/emulated/field_assert_test.go b/std/math/emulated/field_assert_test.go index 91ded024..16e2113c 100644 --- a/std/math/emulated/field_assert_test.go +++ b/std/math/emulated/field_assert_test.go @@ -1,9 +1,10 @@ package emulated import ( + "testing" + "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/test" - "testing" ) type ZeroCircuit[T FieldParams] struct { diff --git a/std/math/emulated/field_binary.go b/std/math/emulated/field_binary.go index d2dd5f3d..7e81f6ae 100644 --- a/std/math/emulated/field_binary.go +++ b/std/math/emulated/field_binary.go @@ -44,11 +44,10 @@ func (f *Field[T]) ToBitsCanonical(a *Element[T]) []frontend.Variable { // `ToBits` after that manually (e.g. in point and scalar marshaling) and // replace them with this method. - var fp T - nbBits := fp.Modulus().BitLen() + nbBits := f.fParams.Modulus().BitLen() // when the modulus is a power of 2, then we can remove the most significant // bit as it is always zero. - if fp.Modulus().TrailingZeroBits() == uint(nbBits-1) { + if f.fParams.Modulus().TrailingZeroBits() == uint(nbBits-1) { nbBits-- } ca := f.ReduceStrict(a) diff --git a/std/math/emulated/field_hint.go b/std/math/emulated/field_hint.go index ac477689..cf2c4d3d 100644 --- a/std/math/emulated/field_hint.go +++ b/std/math/emulated/field_hint.go @@ -144,6 +144,11 @@ func unwrapHint(isEmulatedInput, isEmulatedOutput bool, nativeInputs, nativeOutp // // See the example for full written example. func (f *Field[T]) NewHint(hf solver.Hint, nbOutputs int, inputs ...*Element[T]) ([]*Element[T], error) { + // we need to initialize the inputs before to ensure the constant values are decomposed + // into limbs. If inputs are already initialize, then it is no-op. + for i := range inputs { + inputs[i].Initialize(f.api.Compiler().Field()) + } nativeInputs := f.wrapHint(inputs...) nbNativeOutputs := int(f.fParams.NbLimbs()) * nbOutputs nativeOutputs, err := f.api.Compiler().NewHint(hf, nbNativeOutputs, nativeInputs...) @@ -176,6 +181,11 @@ func (f *Field[T]) NewHint(hf solver.Hint, nbOutputs int, inputs ...*Element[T]) // // in the function we have access to both native and nonantive modulus // })} func (f *Field[T]) NewHintWithNativeOutput(hf solver.Hint, nbOutputs int, inputs ...*Element[T]) ([]frontend.Variable, error) { + // we need to initialize the inputs before to ensure the constant values are decomposed + // into limbs. If inputs are already initialize, then it is no-op. + for i := range inputs { + inputs[i].Initialize(f.api.Compiler().Field()) + } nativeInputs := f.wrapHint(inputs...) nbNativeOutputs := nbOutputs nativeOutputs, err := f.api.Compiler().NewHint(hf, nbNativeOutputs, nativeInputs...) diff --git a/std/math/emulated/field_mul.go b/std/math/emulated/field_mul.go index 8889a3e7..15269a55 100644 --- a/std/math/emulated/field_mul.go +++ b/std/math/emulated/field_mul.go @@ -9,6 +9,7 @@ import ( "github.com/consensys/gnark/frontend" limbs "github.com/consensys/gnark/std/internal/limbcomposition" + "github.com/consensys/gnark/std/math/fieldextension" "github.com/consensys/gnark/std/multicommit" ) @@ -19,6 +20,11 @@ import ( // checks. // // Currently used for multiplication and multivariate evaluation checks. +// +// The methods [evalRound1], [evalRound2] and [check] may receive as inputs +// either [frontend.Variable] or [fieldextension.Element]. The +// implementation should differentiate on the different input types and use the +// appropriate API (native or extension). type deferredChecker interface { // toCommit outputs the variable which should be committed to. The checker // then uses the commitment to obtain the verifier challenge for the @@ -166,9 +172,37 @@ func (mc *mulCheck[T]) check(api frontend.API, peval, coef frontend.Variable) { if mc.p != nil { peval = mc.p.evaluation } - ls := api.Mul(mc.a.evaluation, mc.b.evaluation) - rs := api.Add(mc.r.evaluation, api.Mul(peval, mc.k.evaluation), api.Mul(mc.c.evaluation, coef)) - api.AssertIsEqual(ls, rs) + // we either have to perform the equality check in the native field or in + // the extension field. It was already determined at the [Field] + // initialization time which kind of check needs to be done. + if mc.f.extensionApi == nil { + ls := api.Mul(mc.a.evaluation, mc.b.evaluation) + rs := api.Add(mc.r.evaluation, api.Mul(peval, mc.k.evaluation), api.Mul(mc.c.evaluation, coef)) + api.AssertIsEqual(ls, rs) + } else { + // here we use the fact that [frontend.Variable] is defined as any, but + // we have actually provided [ExtensionVariable]. We type assert to be + // able to use the fieldextension API. + // + // the computations are same as in the previous conditional block, but + // only in the extension. + aext := mc.a.evaluation.(fieldextension.Element) + bext := mc.b.evaluation.(fieldextension.Element) + ls := mc.f.extensionApi.Mul(aext, bext) + + rext := mc.r.evaluation.(fieldextension.Element) + pevalext := peval.(fieldextension.Element) + cext := mc.c.evaluation.(fieldextension.Element) + kext := mc.k.evaluation.(fieldextension.Element) + coefext := coef.(fieldextension.Element) + pkext := mc.f.extensionApi.Mul(pevalext, kext) + ccoefext := mc.f.extensionApi.Mul(coefext, cext) + + rs := mc.f.extensionApi.Add(rext, pkext) + rs = mc.f.extensionApi.Add(rs, ccoefext) + + mc.f.extensionApi.AssertIsEqual(ls, rs) + } } // cleanEvaluations cleans the cached evaluation values. This is necessary for @@ -194,7 +228,7 @@ func (mc *mulCheck[T]) cleanEvaluations() { // defers the actual multiplication check. func (f *Field[T]) mulMod(a, b *Element[T], _ uint, p *Element[T]) *Element[T] { // fast path - if one of the inputs is on zero limbs (it is zero), then the result is also zero - if len(a.Limbs) == 0 || len(b.Limbs) == 0 { + if a.isStrictZero() || b.isStrictZero() { return f.Zero() } f.enforceWidthConditional(a) @@ -220,7 +254,7 @@ func (f *Field[T]) mulMod(a, b *Element[T], _ uint, p *Element[T]) *Element[T] { // checkZero creates multiplication check a * 1 = 0 + k*p. func (f *Field[T]) checkZero(a *Element[T], p *Element[T]) { // fast path - the result is on zero limbs. This means that it is constant zero - if len(a.Limbs) == 0 { + if a.isStrictZero() { return } // the method works similarly to mulMod, but we know that we are multiplying @@ -255,6 +289,18 @@ func (f *Field[T]) evalWithChallenge(a *Element[T], at []frontend.Variable) *Ele if len(at) < len(a.Limbs)-1 { panic("evaluation powers less than limbs") } + var sum frontend.Variable + if f.extensionApi != nil { + sum = f.evalWithChallengeExtension(a, at) + } else { + sum = f.evalWithChallengeNative(a, at) + } + a.isEvaluated = true + a.evaluation = sum + return a +} + +func (f *Field[T]) evalWithChallengeNative(a *Element[T], at []frontend.Variable) frontend.Variable { var sum frontend.Variable = 0 if len(a.Limbs) > 0 { sum = f.api.Mul(a.Limbs[0], 1) // copy because we use MulAcc @@ -262,9 +308,26 @@ func (f *Field[T]) evalWithChallenge(a *Element[T], at []frontend.Variable) *Ele for i := 1; i < len(a.Limbs); i++ { sum = f.api.MulAcc(sum, a.Limbs[i], at[i-1]) } - a.isEvaluated = true - a.evaluation = sum - return a + return sum +} + +func (f *Field[T]) evalWithChallengeExtension(a *Element[T], at []frontend.Variable) frontend.Variable { + // even though at is []frontend.Variable, then we abuse the fact that + // frontend.Variable is defined as any and at is []ExtensionVariable. We + // type assert it. + atext := make([]fieldextension.Element, len(at)) + for i := 0; i < len(at); i++ { + atext[i] = at[i].(fieldextension.Element) + } + sum := f.extensionApi.Zero() + if len(a.Limbs) > 0 { + sum = f.extensionApi.AsExtensionVariable(a.Limbs[0]) + } + for i := 1; i < len(a.Limbs); i++ { + toAdd := f.extensionApi.MulByElement(atext[i-1], a.Limbs[i]) + sum = f.extensionApi.Add(sum, toAdd) + } + return sum } // performDeferredChecks should be deferred to actually perform all the @@ -289,43 +352,91 @@ func (f *Field[T]) performDeferredChecks(api frontend.API) error { for i := range f.deferredChecks { toCommit = append(toCommit, f.deferredChecks[i].toCommit()...) } - // we give all the inputs as inputs to obtain random verifier challenge. - multicommit.WithCommitment(api, func(api frontend.API, commitment frontend.Variable) error { - // for efficiency, we compute all powers of the challenge as slice at. - coefsLen := int(f.fParams.NbLimbs()) - for i := range f.deferredChecks { - coefsLen = max(coefsLen, f.deferredChecks[i].maxLen()) - } - at := make([]frontend.Variable, coefsLen) - at[0] = commitment - for i := 1; i < len(at); i++ { - at[i] = api.Mul(at[i-1], commitment) - } - // evaluate all r, k, c - for i := range f.deferredChecks { - f.deferredChecks[i].evalRound1(at) - } - // assuming r is input to some other multiplication, then is already evaluated - for i := range f.deferredChecks { - f.deferredChecks[i].evalRound2(at) - } - // evaluate p(X) at challenge - pval := f.evalWithChallenge(f.Modulus(), at) - // compute (2^t-X) at challenge - coef := big.NewInt(1) - coef.Lsh(coef, f.fParams.BitsPerLimb()) - ccoef := api.Sub(coef, commitment) - // verify all mulchecks - for i := range f.deferredChecks { - f.deferredChecks[i].check(api, pval.evaluation, ccoef) - } - // clean cached evaluation. Helps in case we compile the same circuit - // multiple times. - for i := range f.deferredChecks { - f.deferredChecks[i].cleanEvaluations() - } - return nil - }, toCommit...) + if f.extensionApi == nil { + // we give all the inputs as inputs to obtain random verifier challenge. + multicommit.WithCommitment(api, func(api frontend.API, commitment frontend.Variable) error { + // for efficiency, we compute all powers of the challenge as slice at. + coefsLen := int(f.fParams.NbLimbs()) + for i := range f.deferredChecks { + coefsLen = max(coefsLen, f.deferredChecks[i].maxLen()) + } + at := make([]frontend.Variable, coefsLen) + at[0] = commitment + for i := 1; i < len(at); i++ { + at[i] = api.Mul(at[i-1], commitment) + } + // evaluate all r, k, c + for i := range f.deferredChecks { + f.deferredChecks[i].evalRound1(at) + } + // assuming r is input to some other multiplication, then is already evaluated + for i := range f.deferredChecks { + f.deferredChecks[i].evalRound2(at) + } + // evaluate p(X) at challenge + pval := f.evalWithChallenge(f.Modulus(), at) + // compute (2^t-X) at challenge + coef := big.NewInt(1) + coef.Lsh(coef, f.fParams.BitsPerLimb()) + ccoef := api.Sub(coef, commitment) + // verify all mulchecks + for i := range f.deferredChecks { + f.deferredChecks[i].check(api, pval.evaluation, ccoef) + } + // clean cached evaluation. Helps in case we compile the same circuit + // multiple times. + for i := range f.deferredChecks { + f.deferredChecks[i].cleanEvaluations() + } + return nil + }, toCommit...) + } else { + // this is the same as above, but we have challenges in the extension + // field. The commitment argument below is actually extension field + // element, but we give it as []frontend.Variable for interface + // compatibility. + multicommit.WithWideCommitment(api, func(api frontend.API, commitment []frontend.Variable) error { + // for efficiency, we compute all powers of the challenge as slice at. + coefsLen := int(f.fParams.NbLimbs()) + for i := range f.deferredChecks { + coefsLen = max(coefsLen, f.deferredChecks[i].maxLen()) + } + at := make([]fieldextension.Element, coefsLen) + at[0] = commitment + for i := 1; i < len(at); i++ { + at[i] = f.extensionApi.Mul(at[i-1], commitment) + } + atv := make([]frontend.Variable, len(at)) + for i := range at { + atv[i] = at[i] + } + // evaluate all r, k, c + for i := range f.deferredChecks { + f.deferredChecks[i].evalRound1(atv) + } + // assuming r is input to some other multiplication, then is already evaluated + for i := range f.deferredChecks { + f.deferredChecks[i].evalRound2(atv) + } + // evaluate p(X) at challenge + pval := f.evalWithChallenge(f.Modulus(), atv) + // compute (2^t-X) at challenge + coef := big.NewInt(1) + coef.Lsh(coef, f.fParams.BitsPerLimb()) + coefext := f.extensionApi.AsExtensionVariable(coef) + ccoef := f.extensionApi.Sub(coefext, commitment) + // verify all mulchecks + for i := range f.deferredChecks { + f.deferredChecks[i].check(api, pval.evaluation, ccoef) + } + // clean cached evaluation. Helps in case we compile the same circuit + // multiple times. + for i := range f.deferredChecks { + f.deferredChecks[i].cleanEvaluations() + } + return nil + }, f.extensionApi.Degree(), toCommit...) + } return nil } @@ -483,7 +594,7 @@ func mulHint(field *big.Int, inputs, outputs []*big.Int) error { // efficient. func (f *Field[T]) Mul(a, b *Element[T]) *Element[T] { // fast path - if one of the inputs is on zero limbs (it is zero), then the result is also zero - if len(a.Limbs) == 0 || len(b.Limbs) == 0 { + if a.isStrictZero() || b.isStrictZero() { return f.Zero() } return f.reduceAndOp(func(a, b *Element[T], u uint) *Element[T] { return f.mulMod(a, b, u, nil) }, f.mulPreCond, a, b) @@ -495,7 +606,7 @@ func (f *Field[T]) Mul(a, b *Element[T]) *Element[T] { // Equivalent to [Field[T].Mul], kept for backwards compatibility. func (f *Field[T]) MulMod(a, b *Element[T]) *Element[T] { // fast path - if one of the inputs is on zero limbs (it is zero), then the result is also zero - if len(a.Limbs) == 0 || len(b.Limbs) == 0 { + if a.isStrictZero() || b.isStrictZero() { return f.Zero() } return f.reduceAndOp(func(a, b *Element[T], u uint) *Element[T] { return f.mulMod(a, b, u, nil) }, f.mulPreCond, a, b) @@ -507,7 +618,7 @@ func (f *Field[T]) MulMod(a, b *Element[T]) *Element[T] { // general [Field[T].Mul] or [Field[T].MulMod] with creating new Element from // the constant on-the-fly. func (f *Field[T]) MulConst(a *Element[T], c *big.Int) *Element[T] { - if len(a.Limbs) == 0 { + if a.isStrictZero() { return f.Zero() } switch c.Sign() { @@ -524,7 +635,7 @@ func (f *Field[T]) MulConst(a *Element[T], c *big.Int) *Element[T] { func(a, _ *Element[T], u uint) *Element[T] { if ba, aConst := f.constantValue(a); aConst { ba.Mul(ba, c) - return newConstElement[T](ba, false) + return newConstElement[T](f.api.Compiler().Field(), ba, false) } limbs := make([]frontend.Variable, len(a.Limbs)) for i := range a.Limbs { @@ -562,7 +673,7 @@ func (f *Field[T]) mulPreCond(a, b *Element[T]) (nextOverflow uint, err error) { // number of limbs of the inputs. func (f *Field[T]) MulNoReduce(a, b *Element[T]) *Element[T] { // fast path - if one of the inputs is on zero limbs (it is zero), then the result is also zero - if len(a.Limbs) == 0 || len(b.Limbs) == 0 { + if a.isStrictZero() || b.isStrictZero() { return f.Zero() } return f.reduceAndOp(f.mulNoReduce, f.mulPreCond, a, b) @@ -585,7 +696,7 @@ func (f *Field[T]) mulNoReduce(a, b *Element[T], nextoverflow uint) *Element[T] // number of limbs and zero overflow. func (f *Field[T]) Exp(base, exp *Element[T]) *Element[T] { // fast path - if the base is zero, then the result is also zero - if len(base.Limbs) == 0 { + if base.isStrictZero() { return f.Zero() } expBts := f.ToBits(exp) @@ -830,19 +941,58 @@ func (mc *mvCheck[T]) evalRound2(at []frontend.Variable) { } } +// check checks that the multivariate polynomial f(x1(ch), x2(ch), ...) = r(ch) +// + k(ch)*p(ch) + (2^t-ch) c(ch) holds. As p and (2^t-ch) are same over all +// checks then we get them as arguments to this method. func (mc *mvCheck[T]) check(api frontend.API, peval, coef frontend.Variable) { - ls := frontend.Variable(0) - for i, term := range mc.mv.Terms { - termProd := frontend.Variable(mc.mv.Coefficients[i]) - for i, pow := range term { - for j := 0; j < pow; j++ { - termProd = api.Mul(termProd, mc.vals[i].evaluation) + // we either have to perform the equality check in the native field or in + // the extension field. It was already determined at the [Field] + // initialization time which kind of check needs to be done. + if mc.f.extensionApi == nil { + ls := frontend.Variable(0) + for i, term := range mc.mv.Terms { + termProd := frontend.Variable(mc.mv.Coefficients[i]) + for i, pow := range term { + for j := 0; j < pow; j++ { + termProd = api.Mul(termProd, mc.vals[i].evaluation) + } } + ls = api.Add(ls, termProd) } - ls = api.Add(ls, termProd) + rs := api.Add(mc.r.evaluation, api.Mul(peval, mc.k.evaluation), api.Mul(mc.c.evaluation, coef)) + api.AssertIsEqual(ls, rs) + } else { + // here we use the fact that [frontend.Variable] is defined as any, but + // we have actually provided [ExtensionVariable]. We type assert to be + // able to use the fieldextension API. + // + // the computations are same as in the previous conditional block, but + // only in the extension. + ls := mc.f.extensionApi.Zero() + for i, term := range mc.mv.Terms { + termProd := mc.f.extensionApi.AsExtensionVariable(mc.mv.Coefficients[i]) + for i, pow := range term { + for j := 0; j < pow; j++ { + valsexti := mc.vals[i].evaluation.(fieldextension.Element) + termProd = mc.f.extensionApi.Mul(termProd, valsexti) + } + } + ls = mc.f.extensionApi.Add(ls, termProd) + } + rext := mc.r.evaluation.(fieldextension.Element) + pevalext := peval.(fieldextension.Element) + kext := mc.k.evaluation.(fieldextension.Element) + cext := mc.c.evaluation.(fieldextension.Element) + coefext := coef.(fieldextension.Element) + + pkext := mc.f.extensionApi.Mul(pevalext, kext) + ccoefext := mc.f.extensionApi.Mul(coefext, cext) + + rs := mc.f.extensionApi.Add(rext, pkext) + rs = mc.f.extensionApi.Add(rs, ccoefext) + + mc.f.extensionApi.AssertIsEqual(ls, rs) } - rs := api.Add(mc.r.evaluation, api.Mul(peval, mc.k.evaluation), api.Mul(mc.c.evaluation, coef)) - api.AssertIsEqual(ls, rs) } func (mc *mvCheck[T]) cleanEvaluations() { @@ -865,7 +1015,6 @@ func (mc *mvCheck[T]) cleanEvaluations() { // As it only depends on the bit-length of the inputs, then we can precompute it // regardless of the actual values. func (f *Field[T]) polyMvEvalQuoSize(mv *multivariate[T], at []*Element[T]) (quoSize uint) { - var fp T quoSizes := make([]uint, len(mv.Terms)) for i, term := range mv.Terms { // for every term, the result length is the sum of the lengths of the @@ -873,7 +1022,7 @@ func (f *Field[T]) polyMvEvalQuoSize(mv *multivariate[T], at []*Element[T]) (quo var lengths []uint for j, pow := range term { for k := 0; k < pow; k++ { - lengths = append(lengths, uint(len(at[j].Limbs))*fp.BitsPerLimb()+at[j].overflow) + lengths = append(lengths, uint(len(at[j].Limbs))*f.fParams.BitsPerLimb()+at[j].overflow) } } lengths = append(lengths, uint(bits.Len(uint(mv.Coefficients[i])))) diff --git a/std/math/emulated/field_ops.go b/std/math/emulated/field_ops.go index 3f688359..2cef2fb5 100644 --- a/std/math/emulated/field_ops.go +++ b/std/math/emulated/field_ops.go @@ -124,7 +124,7 @@ func (f *Field[T]) add(a, b *Element[T], nextOverflow uint) *Element[T] { bb, bConst := f.constantValue(b) if aConst && bConst { ba.Add(ba, bb).Mod(ba, f.fParams.Modulus()) - return newConstElement[T](ba, false) + return newConstElement[T](f.api.Compiler().Field(), ba, false) } nbLimbs := max(len(a.Limbs), len(b.Limbs)) @@ -192,15 +192,14 @@ func (f *Field[T]) sub(a, b *Element[T], nextOverflow uint) *Element[T] { bb, bConst := f.constantValue(b) if aConst && bConst { ba.Sub(ba, bb).Mod(ba, f.fParams.Modulus()) - return newConstElement[T](ba, false) + return newConstElement[T](f.api.Compiler().Field(), ba, false) } // first we have to compute padding to ensure that the subtraction does not // underflow. - var fp T - nbLimbs := max(len(a.Limbs), len(b.Limbs), int(fp.NbLimbs())) + nbLimbs := max(len(a.Limbs), len(b.Limbs), int(f.fParams.NbLimbs())) limbs := make([]frontend.Variable, nbLimbs) - padLimbs := subPadding(fp.Modulus(), fp.BitsPerLimb(), b.overflow, uint(nbLimbs)) + padLimbs := subPadding(f.fParams.Modulus(), f.fParams.BitsPerLimb(), b.overflow, uint(nbLimbs)) for i := range limbs { limbs[i] = padLimbs[i] if i < len(a.Limbs) { @@ -276,7 +275,7 @@ func (f *Field[T]) Lookup2(b0, b1 frontend.Variable, a, b, c, d *Element[T]) *El bNormLimbs := normalize(b.Limbs) cNormLimbs := normalize(c.Limbs) dNormLimbs := normalize(d.Limbs) - for i := range a.Limbs { + for i := range nbLimbs { e.Limbs[i] = f.api.Lookup2(b0, b1, aNormLimbs[i], bNormLimbs[i], cNormLimbs[i], dNormLimbs[i]) } return e @@ -325,7 +324,7 @@ func (f *Field[T]) Mux(sel frontend.Variable, inputs ...*Element[T]) *Element[T] } } e := f.newInternalElement(make([]frontend.Variable, nbLimbs), overflow) - for i := range inputs[0].Limbs { + for i := range nbLimbs { e.Limbs[i] = selector.Mux(f.api, sel, normLimbsTransposed[i]...) } return e diff --git a/std/math/emulated/field_test.go b/std/math/emulated/field_test.go index f685473c..39d7582f 100644 --- a/std/math/emulated/field_test.go +++ b/std/math/emulated/field_test.go @@ -39,8 +39,8 @@ func (c *ConstantCircuit) Define(api frontend.API) error { return err } { - c1 := ValueOf[Secp256k1Fp](42) - b1, ok := f.constantValue(&c1) + c1 := f.NewElement(42) + b1, ok := f.constantValue(c1) if !ok { return errors.New("42 should be constant") } @@ -82,11 +82,11 @@ func (c *MulConstantCircuit) Define(api frontend.API) error { if err != nil { return err } - c0 := ValueOf[Secp256k1Fp](0) - c1 := ValueOf[Secp256k1Fp](0) - c2 := ValueOf[Secp256k1Fp](0) - r := f.Mul(&c0, &c1) - f.AssertIsEqual(r, &c2) + c0 := f.NewElement(0) + c1 := f.NewElement(0) + c2 := f.NewElement(0) + r := f.Mul(c0, c1) + f.AssertIsEqual(r, c2) return nil } @@ -111,14 +111,14 @@ func (c *SubConstantCircuit) Define(api frontend.API) error { if err != nil { return err } - c0 := ValueOf[Secp256k1Fp](0) - c1 := ValueOf[Secp256k1Fp](0) - c2 := ValueOf[Secp256k1Fp](0) - r := f.Sub(&c0, &c1) + c0 := f.NewElement(0) + c1 := f.NewElement(0) + c2 := f.NewElement(0) + r := f.Sub(c0, c1) if r.overflow != 0 { return fmt.Errorf("overflow %d != 0", r.overflow) } - f.AssertIsEqual(r, &c2) + f.AssertIsEqual(r, c2) return nil } diff --git a/std/math/emulated/hints.go b/std/math/emulated/hints.go index 1a220512..e36b8890 100644 --- a/std/math/emulated/hints.go +++ b/std/math/emulated/hints.go @@ -41,15 +41,14 @@ func nbMultiplicationResLimbs(lenLeft, lenRight int) int { // computeInverseHint packs the inputs for the InverseHint hint function. func (f *Field[T]) computeInverseHint(inLimbs []frontend.Variable) (inverseLimbs []frontend.Variable, err error) { - var fp T hintInputs := []frontend.Variable{ - fp.BitsPerLimb(), - fp.NbLimbs(), + f.fParams.BitsPerLimb(), + f.fParams.NbLimbs(), } p := f.Modulus() hintInputs = append(hintInputs, p.Limbs...) hintInputs = append(hintInputs, inLimbs...) - return f.api.NewHint(InverseHint, int(fp.NbLimbs()), hintInputs...) + return f.api.NewHint(InverseHint, int(f.fParams.NbLimbs()), hintInputs...) } // InverseHint computes the inverse x^-1 for the input x and stores it in outputs. @@ -84,10 +83,9 @@ func InverseHint(mod *big.Int, inputs []*big.Int, outputs []*big.Int) error { // computeDivisionHint packs the inputs for DivisionHint hint function. func (f *Field[T]) computeDivisionHint(nomLimbs, denomLimbs []frontend.Variable) (divLimbs []frontend.Variable, err error) { - var fp T hintInputs := []frontend.Variable{ - fp.BitsPerLimb(), - fp.NbLimbs(), + f.fParams.BitsPerLimb(), + f.fParams.NbLimbs(), len(denomLimbs), len(nomLimbs), } @@ -95,7 +93,7 @@ func (f *Field[T]) computeDivisionHint(nomLimbs, denomLimbs []frontend.Variable) hintInputs = append(hintInputs, p.Limbs...) hintInputs = append(hintInputs, nomLimbs...) hintInputs = append(hintInputs, denomLimbs...) - return f.api.NewHint(DivHint, int(fp.NbLimbs()), hintInputs...) + return f.api.NewHint(DivHint, int(f.fParams.NbLimbs()), hintInputs...) } // DivHint computes the value z = x/y for inputs x and y and stores z in diff --git a/std/math/emulated/params.go b/std/math/emulated/params.go index 0b12de10..8126db47 100644 --- a/std/math/emulated/params.go +++ b/std/math/emulated/params.go @@ -26,6 +26,23 @@ type FieldParams interface { Modulus() *big.Int // returns modulus. Do not modify. } +// DynamicFieldParams extends the FieldParams interface to allow for limb size +// and count depending on the native field size. If the field emulation +// parameters do not implement this interface, then the limb size and count are +// fixed to the values defined in the FieldParams interface. +// +// The interface allows for optimized emulation in case the native field is +// large (more than 256 bits) and enables field emulation when the native field +// is small (less than 128 bits). +// +// All defined parameters in the [emparams] package implement this interface. +type DynamicFieldParams interface { + FieldParams + + NbLimbsDynamic(field *big.Int) uint + BitsPerLimbDynamic(field *big.Int) uint +} + type ( Goldilocks = emparams.Goldilocks Secp256k1Fp = emparams.Secp256k1Fp @@ -46,3 +63,59 @@ type ( BabyBear = emparams.BabyBear KoalaBear = emparams.KoalaBear ) + +// ensure that all parameters implement the DynamicFieldParams interface +var ( + _ DynamicFieldParams = (*Goldilocks)(nil) + _ DynamicFieldParams = (*Secp256k1Fp)(nil) + _ DynamicFieldParams = (*Secp256k1Fr)(nil) + _ DynamicFieldParams = (*BN254Fp)(nil) + _ DynamicFieldParams = (*BN254Fr)(nil) + _ DynamicFieldParams = (*BLS12377Fp)(nil) + _ DynamicFieldParams = (*BLS12381Fp)(nil) + _ DynamicFieldParams = (*BLS12381Fr)(nil) + _ DynamicFieldParams = (*P256Fp)(nil) + _ DynamicFieldParams = (*P256Fr)(nil) + _ DynamicFieldParams = (*P384Fp)(nil) + _ DynamicFieldParams = (*P384Fr)(nil) + _ DynamicFieldParams = (*BW6761Fp)(nil) + _ DynamicFieldParams = (*BW6761Fr)(nil) + _ DynamicFieldParams = (*STARKCurveFp)(nil) + _ DynamicFieldParams = (*STARKCurveFr)(nil) + _ DynamicFieldParams = (*BabyBear)(nil) + _ DynamicFieldParams = (*KoalaBear)(nil) +) + +// staticFieldParams is a wrapper to avoid calling the dynamic methods in DynamicFieldParams +// all the time. The native field stays intact and we can cache the values. +type staticFieldParams[T FieldParams] struct { + fp T + nbLimbs, nbBits uint +} + +func newStaticFieldParams[T FieldParams](field *big.Int) staticFieldParams[T] { + var fp T + nbLimbs, nbBits := GetEffectiveFieldParams[T](field) + return staticFieldParams[T]{fp: fp, nbLimbs: nbLimbs, nbBits: nbBits} +} + +func (s *staticFieldParams[T]) Modulus() *big.Int { return s.fp.Modulus() } +func (s *staticFieldParams[T]) IsPrime() bool { return s.fp.IsPrime() } +func (s *staticFieldParams[T]) NbLimbs() uint { return s.nbLimbs } +func (s *staticFieldParams[T]) BitsPerLimb() uint { return s.nbBits } + +// GetEffectiveFieldParams returns the number of limbs and bits per limb for a +// given field. If the field implements the DynamicFieldParams interface, then +// the number of limbs and bits per limb are computed dynamically based on the +// field size. Otherwise, the values are taken from the FieldParams interface. +func GetEffectiveFieldParams[T FieldParams](field *big.Int) (nbLimbs, nbBits uint) { + var fp T + if f, ok := any(fp).(DynamicFieldParams); ok { + nbLimbs = f.NbLimbsDynamic(field) + nbBits = f.BitsPerLimbDynamic(field) + } else { + nbLimbs = fp.NbLimbs() + nbBits = fp.BitsPerLimb() + } + return nbLimbs, nbBits +} diff --git a/std/math/emulated/subtraction_padding.go b/std/math/emulated/subtraction_padding.go index 45482779..e802dac0 100644 --- a/std/math/emulated/subtraction_padding.go +++ b/std/math/emulated/subtraction_padding.go @@ -103,15 +103,14 @@ func (f *Field[T]) computeSubPaddingHint(overflow uint, nbLimbs uint, modulus *E // 1. padding % modulus = 0 // 2. padding[i] >= (1 << (bits+overflow)) // 3. padding[i] + a[i] < native_field for all valid a[i] (defined by overflow) - var fp T - inputs := []frontend.Variable{fp.NbLimbs(), fp.BitsPerLimb(), overflow, nbLimbs} + inputs := []frontend.Variable{f.fParams.NbLimbs(), f.fParams.BitsPerLimb(), overflow, nbLimbs} inputs = append(inputs, modulus.Limbs...) // compute the actual padding value res, err := f.api.NewHint(subPaddingHint, int(nbLimbs), inputs...) if err != nil { panic(fmt.Sprintf("sub padding hint: %v", err)) } - maxLimb := new(big.Int).Lsh(big.NewInt(1), fp.BitsPerLimb()+overflow) + maxLimb := new(big.Int).Lsh(big.NewInt(1), f.fParams.BitsPerLimb()+overflow) maxLimb.Sub(maxLimb, big.NewInt(1)) for i := range res { // we can check conditions 2 and 3 together by subtracting the maximum @@ -120,7 +119,7 @@ func (f *Field[T]) computeSubPaddingHint(overflow uint, nbLimbs uint, modulus *E // at least native_width-overflow) and should be nbBits+overflow+1 bits // wide (as expected padding is one bit wider than the maximum allowed // subtraction limb). - f.checker.Check(f.api.Sub(res[i], maxLimb), int(fp.BitsPerLimb()+overflow+1)) + f.checker.Check(f.api.Sub(res[i], maxLimb), int(f.fParams.BitsPerLimb()+overflow+1)) } // ensure that condition 1 holds diff --git a/std/math/emulated/subtraction_padding_test.go b/std/math/emulated/subtraction_padding_test.go index f7d45401..bca07496 100644 --- a/std/math/emulated/subtraction_padding_test.go +++ b/std/math/emulated/subtraction_padding_test.go @@ -5,6 +5,7 @@ import ( "math/big" "testing" + "github.com/consensys/gnark-crypto/field/babybear" limbs "github.com/consensys/gnark/std/internal/limbcomposition" "github.com/consensys/gnark/test" ) @@ -30,4 +31,17 @@ func testSubPadding[T FieldParams](t *testing.T) { assert.Zero(padValue.Cmp(big.NewInt(0)), "padding not multiple of order") }, fmt.Sprintf("%s/nbLimbs=%d", testName[T](), i)) } + sfp, ok := any(fp).(DynamicFieldParams) + assert.True(ok, "field %T does not implement DynamicFieldParams", fp) + for i := sfp.NbLimbsDynamic(babybear.Modulus()); i < 2*sfp.NbLimbsDynamic(babybear.Modulus()); i++ { + assert.Run(func(assert *test.Assert) { + ls := subPadding(sfp.Modulus(), sfp.BitsPerLimbDynamic(babybear.Modulus()), 0, i) + padValue := new(big.Int) + if err := limbs.Recompose(ls, sfp.BitsPerLimbDynamic(babybear.Modulus()), padValue); err != nil { + assert.FailNow("recompose", err) + } + padValue.Mod(padValue, sfp.Modulus()) + assert.Zero(padValue.Cmp(big.NewInt(0)), "padding not multiple of order") + }, fmt.Sprintf("smallfield/%s/nbLimbs=%d", testName[T](), i)) + } } diff --git a/std/math/fieldextension/default_extensions.go b/std/math/fieldextension/default_extensions.go new file mode 100644 index 00000000..c6b2b4d6 --- /dev/null +++ b/std/math/fieldextension/default_extensions.go @@ -0,0 +1,27 @@ +package fieldextension + +import "math/big" + +var ( + bi0 = big.NewInt(0) + bi1 = big.NewInt(1) + biN3 = big.NewInt(-3) + biN7 = big.NewInt(-7) + biN11 = big.NewInt(-11) +) + +// defaultExtensions gives some default extensions for the small fields defined in gnark. +// They are used when the extension is not explicitly given. +var defaultExtensions = map[string][]*big.Int{ + "2013265921-default": {biN11, bi0, bi0, bi0, bi0, bi0, bi0, bi0, bi1}, // x^8 - 11 -- BabyBear field + "2013265921-8": {biN11, bi0, bi0, bi0, bi0, bi0, bi0, bi0, bi1}, // x^8 - 11 -- BabyBear field + "2013265921-4": {biN11, bi0, bi0, bi0, bi1}, // x^4 - 11 -- BabyBear field + + "2130706433-default": {biN3, bi0, bi0, bi0, bi0, bi0, bi0, bi0, bi1}, // x^8 - 3 -- KoalaBear field + "2130706433-8": {biN3, bi0, bi0, bi0, bi0, bi0, bi0, bi0, bi1}, // x^8 - 3 -- KoalaBear field + "2130706433-4": {biN3, bi0, bi0, bi0, bi1}, // x^4 - 3 -- KoalaBear field + + "18446744069414584321-default": {biN7, bi0, bi0, bi0, bi1}, // x^4 - 7 -- Goldilocks field + "18446744069414584321-4": {biN7, bi0, bi0, bi0, bi1}, // x^4 - 7 -- Goldilocks field + "18446744069414584321-2": {biN7, bi0, bi1}, // x^2 - 7 -- Goldilocks field +} diff --git a/std/math/fieldextension/fieldextension.go b/std/math/fieldextension/fieldextension.go new file mode 100644 index 00000000..b524e862 --- /dev/null +++ b/std/math/fieldextension/fieldextension.go @@ -0,0 +1,247 @@ +// Package fieldextension provides operations over an extension field of the native field. +// +// The operations inside the circuit are performed in the native field. In case +// of small fields, we need to perform some operations over an extension field +// to achieve the required soundness level. This package provides some +// primitives to perform such operations. +// +// NB! This is an experimental package. The API is not stable and may change in +// backwards incompatible way. We also may change the extension construction for +// better performance. +package fieldextension + +import ( + "fmt" + "math/big" + "strconv" + + "github.com/consensys/gnark/frontend" +) + +type extensionType int + +const ( + minimal extensionType = iota // x^n + 1 + simple // x^n + d + generic // everything else +) + +// ext implements the [Field] interface. We have separated the implementation +// and interface to possibly have generic implementation in the future (PLONK +// custom gates). +type ext struct { + api frontend.API + + extension []*big.Int // we expect the extension defining modulus to have small small coefficients + extensionType +} + +// Field is the extension field interface over native field. It provides +// the basic operations over the extension field. +type Field interface { + // Reduce reduces the extension field element modulo the defining polynomial. + Reduce(a Element) Element + // Mul multiplies two extension field elements and reduces the result. + Mul(a, b Element) Element + // MulNoReduce multiplies two extension field elements without reducing the result. + // The degree of the result is the sum of the degrees of the two operands. + MulNoReduce(a, b Element) Element + // Add adds two extension field elements. The result is not reduced. The + // degree of the result is the max of the degrees of the two operands. + Add(a, b Element) Element + // Sub subtracts two extension field elements. The result is not reduced. The + // degree of the result is the max of the degrees of the two operands. + Sub(a, b Element) Element + // MulByElement multiplies an extension field element by a native field + // element. The result is not reduced. The degree of the result is the + // degree of the extension field element. + MulByElement(a Element, b frontend.Variable) Element + // AssertIsEqual asserts that two extension field elements are strictly equal. + // For equality in the extension field, reduce the elements first. + AssertIsEqual(a, b Element) + // Zero returns the zero element of the extension field. By convention it is + // an empty polynomial. + Zero() Element + // One returns the one element of the extension field. By convention it is a + // polynomial of degree 0. + One() Element + // AsExtensionVariable returns the native field element as an extension + // field element of degree 0. + AsExtensionVariable(a frontend.Variable) Element + // Degree returns the degree of the extension field. + Degree() int +} + +// NewExtension returns a new extension field object. +// +// NB! This is experimental API. It is not fully implemented and the interface +// and implementation may change. +func NewExtension(api frontend.API, opts ...Option) (Field, error) { + cfg, err := newConfig(opts...) + if err != nil { + return nil, fmt.Errorf("apply options: %w", err) + } + // extension is provided + if cfg.extension != nil { + et := simple + if cfg.extension[0].Cmp(bi1) == 0 { + et = minimal + } + for i := 1; i < len(cfg.extension)-1; i++ { + if cfg.extension[i].Cmp(bi0) != 0 { + et = generic + break + } + } + return &ext{api: api, extension: cfg.extension, extensionType: et}, nil + } + + // extension is not provided, we try to find a stored one + + // if the degree is not set, then we take the default extension for the given field + degree := "default" + if cfg.degree != -1 { + // otherwise, we take the given degree from the config and try to find the extension + degree = strconv.Itoa(cfg.degree) + } + + extension, ok := defaultExtensions[fmt.Sprintf("%s-%s", api.Compiler().Field(), degree)] + if !ok { + return nil, fmt.Errorf("no default extension for native modulus and not explicit extension provided") + } + return &ext{api: api, extension: extension, extensionType: simple}, nil +} + +// Element is the extension field element. +type Element []frontend.Variable + +func (e *ext) Reduce(a Element) Element { + if e.extensionType == generic { + // TODO: implement later + panic("not implemented") + } + if len(a) < len(e.extension) { + // no reduction needed + return a + } + // we don't want to change a in place + ret := make([]frontend.Variable, len(a)) + copy(ret, a) + for len(ret) >= len(e.extension) { + q := ret[len(e.extension)-1:] + if e.extensionType == simple { + // in case we have minimal extension, we don't need to multiply q by + // the extension + q = e.MulByElement(q, e.api.Neg(e.extension[0])) + } + commonLen := min(len(q), len(e.extension)-1) + for i := range commonLen { + ret[i] = e.api.Add(ret[i], q[i]) + } + for i := commonLen; i < len(q); i++ { + ret[i] = q[i] + } + ret = ret[:max(len(q), len(e.extension)-1)] + } + return ret +} + +func (e *ext) Mul(a, b Element) Element { + ret := e.MulNoReduce(a, b) + return e.Reduce(ret) +} + +func (e *ext) MulNoReduce(a, b Element) Element { + if len(a)+len(b) == 0 { + // both a and b are empty, return empty + return []frontend.Variable{} + } + ret := make([]frontend.Variable, len(a)+len(b)-1) + for i := range ret { + ret[i] = 0 + } + for i := range a { + for j := range b { + ret[i+j] = e.api.Add(ret[i+j], e.api.Mul(a[i], b[j])) + } + } + return ret +} + +func (e *ext) Add(a, b Element) Element { + commonLen := min(len(a), len(b)) + ret := make([]frontend.Variable, max(len(a), len(b))) + for i := range commonLen { + ret[i] = e.api.Add(a[i], b[i]) + } + for i := commonLen; i < len(a); i++ { + ret[i] = a[i] + } + for i := commonLen; i < len(b); i++ { + ret[i] = b[i] + } + return ret +} + +func (e *ext) Sub(a, b Element) Element { + commonLen := min(len(a), len(b)) + ret := make([]frontend.Variable, max(len(a), len(b))) + for i := range commonLen { + ret[i] = e.api.Sub(a[i], b[i]) + } + for i := commonLen; i < len(a); i++ { + ret[i] = a[i] + } + for i := commonLen; i < len(b); i++ { + ret[i] = e.api.Neg(b[i]) + } + return ret +} + +func (e *ext) Div(a, b Element) Element { + panic("not implemented") +} + +func (e *ext) Inverse(a Element) Element { + // in case it will be implemented, then also allow rangechecker to be used with WideCommitment + // For that, remove the explicit panic in `std/rangecheck/rangecheck.go` and start using + // WithWideCommitment in `std/internal/logderivarg/logderivarg.go`. + panic("not implemented") +} + +func (e *ext) MulByElement(a Element, b frontend.Variable) Element { + ret := make([]frontend.Variable, len(a)) + for i := range a { + ret[i] = e.api.Mul(a[i], b) + } + return ret +} + +func (e *ext) AssertIsEqual(a, b Element) { + commonLen := min(len(a), len(b)) + for i := range commonLen { + e.api.AssertIsEqual(a[i], b[i]) + } + for i := commonLen; i < len(a); i++ { + e.api.AssertIsEqual(a[i], 0) + } + for i := commonLen; i < len(b); i++ { + e.api.AssertIsEqual(b[i], 0) + } +} + +func (e *ext) Zero() Element { + return []frontend.Variable{} +} + +func (e *ext) One() Element { + return []frontend.Variable{1} +} + +func (e *ext) AsExtensionVariable(a frontend.Variable) Element { + return []frontend.Variable{a} +} + +func (e *ext) Degree() int { + return len(e.extension) - 1 +} diff --git a/std/math/fieldextension/fieldextension_test.go b/std/math/fieldextension/fieldextension_test.go new file mode 100644 index 00000000..dae14ed9 --- /dev/null +++ b/std/math/fieldextension/fieldextension_test.go @@ -0,0 +1,212 @@ +package fieldextension + +import ( + "testing" + + "github.com/consensys/gnark-crypto/field/babybear" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/test" +) + +type ReduceCircut struct { + Input []frontend.Variable + Reduced []frontend.Variable +} + +func (c *ReduceCircut) Define(api frontend.API) error { + e, err := NewExtension(api) + if err != nil { + return err + } + res := e.Reduce(c.Input) + e.AssertIsEqual(c.Reduced, res) + return nil +} + +func TestReduce(t *testing.T) { + assert := test.NewAssert(t) + for _, tc := range []struct { + input, reduced []int + }{ + {[]int{1467980320, 1137445292}, []int{1467980320, 1137445292, 0, 0, 0, 0, 0, 0}}, + {[]int{1906025257, 900972831, 355994451}, []int{1906025257, 900972831, 355994451, 0, 0, 0, 0, 0}}, + {[]int{1315269736, 1305411155, 1484949641, 1487157818}, []int{1315269736, 1305411155, 1484949641, 1487157818, 0, 0, 0, 0}}, + {[]int{930655562, 191916507, 245232235, 249903878, 1688769114}, []int{930655562, 191916507, 245232235, 249903878, 1688769114, 0, 0, 0}}, + {[]int{1900558240, 1034669852, 62012066, 1636768938, 1951223124, 800157949}, []int{1900558240, 1034669852, 62012066, 1636768938, 1951223124, 800157949, 0, 0}}, + {[]int{1506768621, 1188015241, 521233244, 464809937, 288133325, 339109914, 1107846641}, []int{1506768621, 1188015241, 521233244, 464809937, 288133325, 339109914, 1107846641, 0}}, + + {[]int{1467980320, 1137445292}, []int{1467980320, 1137445292}}, + {[]int{1906025257, 900972831, 355994451}, []int{1906025257, 900972831, 355994451}}, + {[]int{1315269736, 1305411155, 1484949641, 1487157818}, []int{1315269736, 1305411155, 1484949641, 1487157818}}, + {[]int{930655562, 191916507, 245232235, 249903878, 1688769114}, []int{930655562, 191916507, 245232235, 249903878, 1688769114}}, + {[]int{1900558240, 1034669852, 62012066, 1636768938, 1951223124, 800157949}, []int{1900558240, 1034669852, 62012066, 1636768938, 1951223124, 800157949}}, + {[]int{1506768621, 1188015241, 521233244, 464809937, 288133325, 339109914, 1107846641}, []int{1506768621, 1188015241, 521233244, 464809937, 288133325, 339109914, 1107846641}}, + + {[]int{1200147517, 527805146, 1459729161, 298883860, 1301476489, 186161068, 997795829, 257063407}, []int{1200147517, 527805146, 1459729161, 298883860, 1301476489, 186161068, 997795829, 257063407}}, + {[]int{1353990425, 388912686, 1299455585, 514865345, 286702144, 1363798779, 1209821622, 492855042, 1874476453}, []int{1840572198, 388912686, 1299455585, 514865345, 286702144, 1363798779, 1209821622, 492855042}}, + {[]int{953116276, 1677525413, 1330847726, 935325903, 367765685, 666819005, 1259969643, 141562180, 860612033, 1047773391}, []int{353519034, 1123437188, 1330847726, 935325903, 367765685, 666819005, 1259969643, 141562180}}, + {[]int{752482022, 867506333, 1723423219, 361328340, 1241112226, 476919145, 182725336, 1468842972, 551661607, 617211228, 590726493}, []int{780961936, 1617032078, 168350958, 361328340, 1241112226, 476919145, 182725336, 1468842972}}, + {[]int{1013674053, 1587348044, 1207155881, 1116555932, 478056632, 1288268012, 1451373934, 1796131301, 1248869310, 1814778483, 275039764, 1209127427}, []int{658375016, 1417252147, 206061443, 324096182, 478056632, 1288268012, 1451373934, 1796131301}}, + {[]int{1498948171, 1003564318, 56559900, 1147491866, 1124826785, 1729654757, 819256679, 1503546020, 1907968762, 427148195, 1043561808, 666209549, 911979384}, []int{340679422, 1675662621, 1469410183, 422733221, 1090270404, 1729654757, 819256679, 1503546020}}, + {[]int{681684962, 586522244, 2004199348, 221839431, 1345587360, 872049662, 1613021061, 1424383966, 558639729, 930888084, 1820774825, 932126772, 391304517, 456497888}, []int{786924218, 759961563, 1900063213, 408904318, 1623405205, 1866994588, 1613021061, 1424383966}}, + {[]int{1727523785, 1360879540, 1735135715, 148864715, 863920986, 1616761360, 984599128, 940289252, 1438501308, 1482499975, 491492725, 1154531434, 1826009304, 581938247, 1777373046}, []int{1444910805, 1562251897, 1101757927, 769114963, 817364120, 1978284314, 403043424, 940289252}}, + {[]int{877033825, 65655943, 178692522, 262720537, 1210970053, 1422087058, 1186945353, 1031788918, 1005627592, 1704405255, 1402192569, 1123357903, 932685461, 1556434039, 1727137317, 1394736528}, []int{1872607732, 694720459, 1509949334, 540061944, 1404180519, 423468198, 52796630, 267763358}}, + {[]int{1134977302, 1648475213, 477296831, 768465845, 11704608, 542135288, 1210705842, 1659979380, 1241159085, 1275966185, 1014704952, 526651747, 1562882049, 1877460597, 1053879427, 1280810580, 1520192977}, []int{1431017196, 1591241801, 1572721698, 521837299, 1097279779, 1061542645, 723784013, 1656034313}}, + {[]int{1555011839, 787815769, 182103774, 781857749, 471054316, 962296659, 1274815353, 1018400075, 1523982888, 1230566410, 1442405474, 639520646, 1147075418, 1454417950, 191443111, 424358887, 598662772, 1646007563}, []int{160052574, 84773776, 1955702541, 1776787092, 1009288388, 854766741, 1367423653, 1659815990}}, + {[]int{1779267173, 1543263614, 1661719435, 1900359777, 893592452, 1006662846, 1442338151, 1005387230, 1302908503, 1159753495, 1351425996, 1816874489, 1790280276, 1100455927, 955172652, 726156767, 566008880, 282490572, 1152386475}, []int{41166504, 163529167, 944692949, 1753319946, 454016278, 1032082517, 1882907718, 940047983}}, + {[]int{1002186586, 435254065, 1396145151, 1622293879, 174136851, 1037681320, 768511646, 675947902, 188213580, 1414683255, 1986089618, 1203380915, 1567490308, 36246439, 1651769094, 105294241, 1140394104, 1184467882, 986378799, 1295577854}, []int{131608080, 269375833, 1666351158, 496800993, 1310402871, 1436392149, 818578391, 1834184553}}, + } { + bb8 := make([]frontend.Variable, len(tc.input)) + for i := range tc.input { + bb8[i] = frontend.Variable(tc.input[i]) + } + bb8red := make([]frontend.Variable, len(tc.reduced)) + for i := range tc.reduced { + bb8red[i] = frontend.Variable(tc.reduced[i]) + } + err := test.IsSolved(&ReduceCircut{Input: make([]frontend.Variable, len(bb8)), Reduced: make([]frontend.Variable, len(bb8red))}, + &ReduceCircut{Input: bb8, Reduced: bb8red}, babybear.Modulus()) + assert.NoError(err) + } +} + +type AddCircuit struct { + A, B, C []frontend.Variable +} + +func (c *AddCircuit) Define(api frontend.API) error { + e, err := NewExtension(api) + if err != nil { + return err + } + res := e.Add(c.A, c.B) + e.AssertIsEqual(c.C, res) + return nil +} + +func TestAdd(t *testing.T) { + assert := test.NewAssert(t) + for _, tc := range []struct { + a, b, c []int + }{ + {[]int{1504941483, 528713979, 1590716977, 1030723568, 691448958, 45161890, 558331570, 1584182780, 884750304, 1178012232, 1236551897, 1743822194, 1102524691, 949136580, 968686988, 1807636110, 1419005839}, []int{1451698632, 267757499, 1153206782, 291258043, 1014114345, 588561574, 161218185, 1655775873, 115681370, 24609626, 1495418674}, []int{943374194, 796471478, 730657838, 1321981611, 1705563303, 633723464, 719549755, 1226692732, 1000431674, 1202621858, 718704650, 1743822194, 1102524691, 949136580, 968686988, 1807636110, 1419005839}}, + {[]int{1874099641, 1982653637, 1187310579, 475561226, 1692092055}, []int{1230758452, 1959413289, 1645368110, 432360750, 1418838351, 687464797, 1234750833, 1203209996, 888838337, 852882006, 356386082, 916503764, 1792596122, 1102186785, 1444663299}, []int{1091592172, 1928801005, 819412768, 907921976, 1097664485, 687464797, 1234750833, 1203209996, 888838337, 852882006, 356386082, 916503764, 1792596122, 1102186785, 1444663299}}, + {[]int{1795185782, 1766445854, 504379178, 1820376092, 137151794, 1064960087, 1759175291, 585123542, 1604030370, 1511659175, 916198528, 1166864589, 1699685308}, []int{805729528, 430124370, 1260617837, 297604025, 613457793, 20971739, 105513811}, []int{587649389, 183304303, 1764997015, 104714196, 750609587, 1085931826, 1864689102, 585123542, 1604030370, 1511659175, 916198528, 1166864589, 1699685308}}, + {[]int{370766974, 1556330301, 1310468525, 1225434398, 928378540, 435540789, 361405873, 1035503425, 545600368, 120758801, 1022518983, 1758884239, 1312473265, 1134254141}, []int{1297416854, 637666735, 325808988, 824671410}, []int{1668183828, 180731115, 1636277513, 36839887, 928378540, 435540789, 361405873, 1035503425, 545600368, 120758801, 1022518983, 1758884239, 1312473265, 1134254141}}, + {[]int{1891025422, 2009813156, 1706954798, 1389626918, 1029725850, 1487402244, 717521687, 10632936, 73787955, 744460996, 1457784272, 1484874357, 811933684, 1652886077, 1531756184, 753745186, 1714652400}, []int{457901568, 944951453, 169650164, 315210583, 1580068898, 1204321039, 648114211, 1202582296, 197451510, 734577008, 1745397260, 1991793135, 1515312634, 736548227, 1072265360, 1703339801, 5096947, 881796514}, []int{335661069, 941498688, 1876604962, 1704837501, 596528827, 678457362, 1365635898, 1213215232, 271239465, 1479038004, 1189915611, 1463401571, 313980397, 376168383, 590755623, 443819066, 1719749347, 881796514}}, + {[]int{1621710603, 226525544, 1202575715}, []int{1412522468, 178072249, 1954193329, 164698463, 2004081065, 1337457847, 1308872918}, []int{1020967150, 404597793, 1143503123, 164698463, 2004081065, 1337457847, 1308872918}}, + {[]int{409456482, 1543428783, 135589462, 1688687654, 1313059883, 348554791, 299198720, 1323721072, 1389838688, 822515643, 927970864, 1040608757, 1776611271, 1797713807, 712571504, 775475735, 1363147356, 787062335, 734743186, 334849816}, []int{1779280006, 450050841, 889363814, 1440765181, 1194153487, 1482798286, 28525033, 743091086, 1967868359, 423958824, 259288007, 640076739, 873173657, 1402881862, 627946497, 315209236, 676276018, 1482056562, 107131096, 295273407}, []int{175470567, 1993479624, 1024953276, 1116186914, 493947449, 1831353077, 327723753, 53546237, 1344441126, 1246474467, 1187258871, 1680685496, 636519007, 1187329748, 1340518001, 1090684971, 26157453, 255852976, 841874282, 630123223}}, + {[]int{439132273, 1435362348, 652986404, 595027578, 50394610, 1163471868, 1350110751, 1387888121, 1541711601, 1311011531, 629723242, 332422020, 1846595946, 1630183415, 892729502, 29895452, 1044010203}, []int{1813396452, 114068876, 1327268679, 1868447085, 184894747, 1182003852}, []int{239262804, 1549431224, 1980255083, 450208742, 235289357, 332209799, 1350110751, 1387888121, 1541711601, 1311011531, 629723242, 332422020, 1846595946, 1630183415, 892729502, 29895452, 1044010203}}, + {[]int{166180982, 764991101, 689087390, 429838129, 645158827, 1453030567, 1933567468, 1814820989, 457070860, 1832972348, 222162489, 312570738, 1353658637, 97753143, 1606729033, 596918423, 1097411730}, []int{682845415, 1075084129, 166827081, 1149467700, 750197496, 1980081828, 137604657, 584718339, 1309044764, 1639753374, 1544780495, 1889342289}, []int{849026397, 1840075230, 855914471, 1579305829, 1395356323, 1419846474, 57906204, 386273407, 1766115624, 1459459801, 1766942984, 188647106, 1353658637, 97753143, 1606729033, 596918423, 1097411730}}, + } { + bb8a := make([]frontend.Variable, len(tc.a)) + for i := range tc.a { + bb8a[i] = frontend.Variable(tc.a[i]) + } + bb8b := make([]frontend.Variable, len(tc.b)) + for i := range tc.b { + bb8b[i] = frontend.Variable(tc.b[i]) + } + bb8c := make([]frontend.Variable, len(tc.c)) + for i := range tc.c { + bb8c[i] = frontend.Variable(tc.c[i]) + } + err := test.IsSolved(&AddCircuit{A: make([]frontend.Variable, len(bb8a)), B: make([]frontend.Variable, len(bb8b)), C: make([]frontend.Variable, len(bb8c))}, + &AddCircuit{A: bb8a, B: bb8b, C: bb8c}, babybear.Modulus()) + assert.NoError(err) + } +} + +type SubCircuit struct { + A, B, C []frontend.Variable +} + +func (c *SubCircuit) Define(api frontend.API) error { + e, err := NewExtension(api) + if err != nil { + return err + } + res := e.Sub(c.A, c.B) + e.AssertIsEqual(c.C, res) + return nil +} + +func TestSub(t *testing.T) { + assert := test.NewAssert(t) + for _, tc := range []struct { + a, b, c []int + }{ + {[]int{1146194893, 161636653, 1838869339, 53943494, 240077858, 1545249092, 1809326915, 1715283441, 1371628, 294589792, 350818866, 391858895, 1629176799, 601342455, 1570046548, 1407018614, 116964098}, []int{1358850047, 1241999865, 899127662}, []int{1800610767, 932902709, 939741677, 53943494, 240077858, 1545249092, 1809326915, 1715283441, 1371628, 294589792, 350818866, 391858895, 1629176799, 601342455, 1570046548, 1407018614, 116964098}}, + {[]int{1930372684, 1864892085, 1136595379, 1655262918, 778003842, 1395703951, 674238279, 303428310, 1869785911, 1465648550, 1654265669, 601993522, 1573728473, 678122861}, []int{758288169, 655811754, 1808890303}, []int{1172084515, 1209080331, 1340970997, 1655262918, 778003842, 1395703951, 674238279, 303428310, 1869785911, 1465648550, 1654265669, 601993522, 1573728473, 678122861}}, + {[]int{198803940, 683262254, 1171724940, 220582, 1436309010, 1011767254, 1619789563, 984205254, 1230618647, 661342751, 1574746193, 850095862, 1888386567}, []int{29202675, 1965459445, 1226138134, 614755, 823163111, 1965257586, 570492890, 714310672, 1863719043, 316112110, 751275028, 1305876957, 76087403, 289554855, 543603956, 1343584811}, []int{169601265, 731068730, 1958852727, 2012871748, 613145899, 1059775589, 1049296673, 269894582, 1380165525, 345230641, 823471165, 1557484826, 1812299164, 1723711066, 1469661965, 669681110}}, + {[]int{867771133, 1674871834, 173849765, 1667039402, 1926702105, 192555144}, []int{793120622, 876063077, 577433800, 1846006825, 1905707677, 1851151225}, []int{74650511, 798808757, 1609681886, 1834298498, 20994428, 354669840}}, + {[]int{56091872, 813716739, 362113363, 1053599731, 178619716, 1801257436, 864815551, 1305284265, 340955220, 1066690326, 674386095, 370881527, 1974134341, 167570042, 1480417387, 190897437}, []int{414748620, 1946157966, 678505871, 1157487387, 1854184016, 438292057, 1226900614, 2009898878, 557555644, 1058000961, 951280428, 1740323340, 1389148174, 315149809, 1822366716, 1274014418, 1803141600, 27865225}, []int{1654609173, 880824694, 1696873413, 1909378265, 337701621, 1362965379, 1651180858, 1308651308, 1796665497, 8689365, 1736371588, 643824108, 584986167, 1865686154, 1671316592, 930148940, 210124321, 1985400696}}, + {[]int{894252018, 1208416601, 802813920, 406175937, 1248756763, 2010718340, 132883210, 808520913}, []int{521827370, 428787881, 1443028395, 248442971, 1526599792, 1784112161, 1259960262, 1432566078, 234210554, 377567478, 1616559930, 1457879671, 1783692545, 1166700134, 63192557, 238060092, 1077493263}, []int{372424648, 779628720, 1373051446, 157732966, 1735422892, 226606179, 886188869, 1389220756, 1779055367, 1635698443, 396705991, 555386250, 229573376, 846565787, 1950073364, 1775205829, 935772658}}, + {[]int{1222756305, 1532801094, 1965391915, 1635685881, 1432129702, 1842258559, 818133559, 126161692, 1872764052, 1885587202, 388899896, 1271969485, 1753820414, 551808295, 272431669, 879739774, 672550552}, []int{296928275, 1436034937, 1801721783, 1498823779, 841763593, 248672479, 124418116, 1495721918, 555622041, 962101046, 1267239367, 1607045139, 1006652808, 369825252, 1129445804}, []int{925828030, 96766157, 163670132, 136862102, 590366109, 1593586080, 693715443, 643705695, 1317142011, 923486156, 1134926450, 1678190267, 747167606, 181983043, 1156251786, 879739774, 672550552}}, + {[]int{1731622955, 615410865, 1558496679, 195832953, 78170750, 61301540, 424972314, 1058412714}, []int{1077167652, 248376566, 1905047628, 1483682839, 135881338, 1082317338, 975917104, 914666340}, []int{654455303, 367034299, 1666714972, 725416035, 1955555333, 992250123, 1462321131, 143746374}}, + {[]int{1362372527, 1429758972, 1923199203, 808799268, 908434557, 22885471, 289022981, 655969201, 944182779, 947702885}, []int{1640391773, 1285351917, 1033611649, 157640943, 584694384}, []int{1735246675, 144407055, 889587554, 651158325, 323740173, 22885471, 289022981, 655969201, 944182779, 947702885}}, + } { + bb8a := make([]frontend.Variable, len(tc.a)) + for i := range tc.a { + bb8a[i] = frontend.Variable(tc.a[i]) + } + bb8b := make([]frontend.Variable, len(tc.b)) + for i := range tc.b { + bb8b[i] = frontend.Variable(tc.b[i]) + } + bb8c := make([]frontend.Variable, len(tc.c)) + for i := range tc.c { + bb8c[i] = frontend.Variable(tc.c[i]) + } + err := test.IsSolved(&SubCircuit{A: make([]frontend.Variable, len(bb8a)), B: make([]frontend.Variable, len(bb8b)), C: make([]frontend.Variable, len(bb8c))}, + &SubCircuit{A: bb8a, B: bb8b, C: bb8c}, babybear.Modulus()) + assert.NoError(err) + } +} + +type MulCircuit struct { + A, B, C []frontend.Variable +} + +func (c *MulCircuit) Define(api frontend.API) error { + e, err := NewExtension(api) + if err != nil { + return err + } + res := e.Mul(c.A, c.B) + e.AssertIsEqual(c.C, res) + return nil +} + +func TestMul(t *testing.T) { + assert := test.NewAssert(t) + for _, tc := range []struct { + a, b, c []int + }{ + {[]int{234968604, 1416371157, 1226800682, 893689929, 1778035510, 146580532, 280014629, 1865717137, 982812264, 531104756, 624717176}, []int{1870372928, 89929324, 1716676259}, []int{1906632739, 115903316, 672362298, 305415989, 834985591, 1605817228, 1210941820, 985790928}}, + {[]int{1087077945, 320581995, 1629282702, 1741108544, 1040857706, 1916768501, 1565495085, 823889356, 1417428004, 1583630854, 1114754081, 1910869750, 187917565, 1438312600}, []int{1563899835, 168797949, 1371079710, 2987340, 1026622935, 1246885219, 506032556, 1788593166, 237013976, 1824355399, 625048497, 68448670, 1607339381, 951954832, 885388282, 683432779, 1575631187}, []int{720142330, 1056607600, 416577423, 1478261035, 1220299325, 903507263, 959938193, 355726286}}, + {[]int{1270288480, 1120584133, 721331187, 1421659182, 1094444484, 359616929, 969570910, 1882596876, 1297123805, 1881461151, 97448081}, []int{2003818742, 1858628164, 1023684969, 1085350554, 781453742, 1116677995, 1468065106, 1335317024, 1486544729, 1673869660, 144423861}, []int{426591007, 224682575, 778802683, 1271911177, 251644533, 207528538, 1964476679, 1876339154}}, + {[]int{1853783118, 1380960591, 964095257, 695279244, 315564693, 1867490771, 53851649, 1343775624, 653780889, 1583674803}, []int{1969070769, 1769394471, 414599120, 647597532, 1788546055, 224442741, 1412932412, 680401167, 298718932, 1146328071, 1478899454, 1909103677, 1428990649, 1439633502, 54662272, 596249162, 461878709, 563248862, 1000459500, 1645847614}, []int{504670415, 227198315, 1349561269, 501560516, 894895922, 1993202942, 1850127592, 1108750151}}, + {[]int{414037409, 953085481, 1924772772, 1517340116, 1237653110, 133837088, 1315588440, 238864701}, []int{1304959022, 1925100119, 978981709, 1918377397, 1207231558, 281134995, 502889770}, []int{142108887, 1889729586, 1250323514, 594024056, 1230999660, 1787836861, 1534177366, 801580624}}, + {[]int{947344343, 423823149, 344707902, 700248832, 566581327, 1849547514, 399209144, 1091850846, 1364174972, 1803614392, 1634840199, 1026184357, 1838704001, 203731055, 743992513, 251080705, 1036651012, 759652320, 577883317, 1716209722, 1529813228}, []int{886233417, 796245045, 174590451, 434936528, 626331990}, []int{1530952022, 551906495, 1612686281, 1586062699, 971563806, 686771089, 907205414, 1181573107}}, + {[]int{7027166, 463591444, 1803846561, 1505438619, 2012281334, 1039204555, 1439978503, 1620975569, 870977727, 746630744, 1686836478, 1924796057}, []int{819644881, 463949651, 522020012, 1377665054, 831007978, 1954765014, 1976440214, 1392258642, 52259004, 1536634317, 1591661847, 628460335, 150161825, 1915169606, 671751539, 196434398, 1160799204, 1385730435, 583362563}, []int{128480043, 456509411, 299825145, 1189434742, 1790453058, 918340297, 1075473370, 992322875}}, + {[]int{1711268919, 1677353510}, []int{315387358, 877475853, 1779977986, 1816170934, 1740575889, 1377265373, 2007938566, 486612909, 953317838, 150150087, 1034065308, 130344828, 1720755480, 1194766973, 74573519, 511551933, 1766944307, 214027799, 226716130, 1055958601, 1902536491}, []int{1633902657, 998090610, 1658849111, 175196657, 889509699, 1868699941, 1847876682, 1348099822}}, + {[]int{1926641336, 233517016, 1382898361, 516240145, 730324703, 196139649, 1751487986, 1718388392, 93866265, 234489342, 1447664327, 978489786, 629636261}, []int{1812584753, 1146117727, 185071390}, []int{1032160623, 1983522902, 1373330365, 1300425158, 649650457, 1711009205, 567980350, 546199098}}, + } { + bb8a := make([]frontend.Variable, len(tc.a)) + for i := range tc.a { + bb8a[i] = frontend.Variable(tc.a[i]) + } + bb8b := make([]frontend.Variable, len(tc.b)) + for i := range tc.b { + bb8b[i] = frontend.Variable(tc.b[i]) + } + bb8c := make([]frontend.Variable, len(tc.c)) + for i := range tc.c { + bb8c[i] = frontend.Variable(tc.c[i]) + } + err := test.IsSolved(&MulCircuit{A: make([]frontend.Variable, len(bb8a)), B: make([]frontend.Variable, len(bb8b)), C: make([]frontend.Variable, len(bb8c))}, + &MulCircuit{A: bb8a, B: bb8b, C: bb8c}, babybear.Modulus()) + assert.NoError(err) + } +} diff --git a/std/math/fieldextension/option.go b/std/math/fieldextension/option.go new file mode 100644 index 00000000..e09e3680 --- /dev/null +++ b/std/math/fieldextension/option.go @@ -0,0 +1,63 @@ +package fieldextension + +import ( + "fmt" + "math/big" +) + +type config struct { + extension []*big.Int + degree int +} + +// Option allows to configure the extension field at initialization time. +type Option func(*config) error + +// WithDegree forces the degree of the extension field. If not set then we +// choose the degree which provides soundness over the native field. +// +// This option is a no-op when the extension is provided with the +// [WithExtension] option. +func WithDegree(degree int) Option { + return func(c *config) error { + if degree < 0 { + return fmt.Errorf("degree must be non-negative") + } + c.degree = degree + return nil + } +} + +// WithExtension sets the extension of the field. The input should be a slice of +// the polynomial coefficients defining the extension in LSB order. The +// coefficient of the highest degree must be 1. +// +// Example, the extension x^3 + 2x^2 + 3x + 1 is represented as +// +// [1, 3, 2, 1]. +// +// This option overrides the [WithDegree] option. +func WithExtension(extension []*big.Int) Option { + return func(c *config) error { + if len(extension) == 0 { + return fmt.Errorf("extension must be non-empty") + } + if extension[len(extension)-1].Cmp(bi1) != 0 { + return fmt.Errorf("last coefficient of the extension must be 1") + } + c.extension = extension + return nil + } +} + +func newConfig(opts ...Option) (*config, error) { + c := &config{ + degree: -1, + } + for _, opt := range opts { + if err := opt(c); err != nil { + return nil, err + } + } + return c, nil +} diff --git a/std/math/polynomial/polynomial_test.go b/std/math/polynomial/polynomial_test.go index 4fd29295..6a83e570 100644 --- a/std/math/polynomial/polynomial_test.go +++ b/std/math/polynomial/polynomial_test.go @@ -224,7 +224,7 @@ func (c *TestPartialMultilinearEvalCircuit[FR]) Define(api frontend.API) error { } ones := make([]emulated.Element[FR], 1< 0 { + panic("working with small field and there are callbacks for single-element commitment") + } + rootCmt, err := committer.WideCommit(mct.maxWidth, mct.vars...) + if err != nil { + return fmt.Errorf("wide commit: %w", err) + } + fe, err := fieldextension.NewExtension(api, fieldextension.WithDegree(mct.maxWidth)) + if err != nil { + return fmt.Errorf("create field extension: %w", err) + } + cmt := rootCmt + for i := range len(mct.wcbs) { + if i > 0 { + cmt = fe.Mul(cmt, rootCmt) + } + if err := mct.wcbs[i].cb(api, cmt[:mct.wcbs[i].width]); err != nil { + return fmt.Errorf("wide callback %d: %w", i, err) + } + } + } else { + // we compile over a large field. In this case we can use the [frontend.Committer] + // interface. We also check that the there are no wide callbacks with [WithWideCommitment] method + // as the caller should be able to expand the commitment into multiple values themselves. + committer, ok := api.Compiler().(frontend.Committer) + if !ok { + panic("compiler doesn't implement frontend.Committer") + } + if len(mct.wcbs) > 0 { + panic("working with large field and there are callbacks for wide commitment") + } + rootCmt, err := committer.Commit(mct.vars...) + if err != nil { + return fmt.Errorf("commit: %w", err) + } + cmt := rootCmt + for i := range len(mct.cbs) { + if i > 0 { + cmt = api.Mul(rootCmt, cmt) + } + if err := mct.cbs[i](api, cmt); err != nil { + return fmt.Errorf("callback %d: %w", i, err) + } } } return nil @@ -115,6 +162,10 @@ func (mct *multicommitter) commitAndCall(api frontend.API) error { // leads to panic. However, the method can call defer for other callbacks. type WithCommitmentFn func(api frontend.API, commitment frontend.Variable) error +// WithWideCommitmentFn is as [WidthCommitmentFn], but instead receives a slice +// of commitments. The commitments is generated in the extension field. +type WithWideCommitmentFn func(api frontend.API, commitment []frontend.Variable) error + // WithCommitment schedules the function cb to be called with a unique // commitment. We append the variables committedVariables to be committed to // with the native [frontend.Committer] interface. @@ -126,3 +177,13 @@ func WithCommitment(api frontend.API, cb WithCommitmentFn, committedVariables .. mct.vars = append(mct.vars, committedVariables...) mct.cbs = append(mct.cbs, cb) } + +func WithWideCommitment(api frontend.API, cb WithWideCommitmentFn, width int, committedVariable ...frontend.Variable) { + mct := getCached(api) + if mct.closed { + panic("called WithCommitment recursively") + } + mct.maxWidth = max(mct.maxWidth, width) + mct.vars = append(mct.vars, committedVariable...) + mct.wcbs = append(mct.wcbs, wcbInfo{cb: cb, width: width}) +} diff --git a/std/multicommit/nativecommit_test.go b/std/multicommit/nativecommit_test.go index b78f5186..84de07ee 100644 --- a/std/multicommit/nativecommit_test.go +++ b/std/multicommit/nativecommit_test.go @@ -4,8 +4,12 @@ import ( "testing" "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/koalabear" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/frontend/cs/scs" + "github.com/consensys/gnark/internal/widecommitter" + "github.com/consensys/gnark/std/math/fieldextension" "github.com/consensys/gnark/test" ) @@ -51,7 +55,7 @@ func TestMultipleCommitments(t *testing.T) { circuit := multipleCommitmentCircuit{} assignment := multipleCommitmentCircuit{X: 10} assert := test.NewAssert(t) - assert.ProverSucceeded(&circuit, &assignment, test.WithCurves(ecc.BN254)) // right now PLONK doesn't implement commitment + assert.ProverSucceeded(&circuit, &assignment, test.WithCurves(ecc.BN254)) } type noCommitVariable struct { @@ -71,3 +75,58 @@ func TestNoCommitVariable(t *testing.T) { assert := test.NewAssert(t) assert.ProverSucceeded(&circuit, &assignment, test.WithCurves(ecc.BN254)) } + +type wideCommitment struct { + X frontend.Variable + withCommitment bool +} + +func (c *wideCommitment) Define(api frontend.API) error { + if c.withCommitment { + WithCommitment(api, func(api frontend.API, commitment frontend.Variable) error { + api.AssertIsDifferent(commitment, 0) + return nil + }, c.X) + } + WithWideCommitment(api, func(api frontend.API, commitment []frontend.Variable) error { + fe, err := fieldextension.NewExtension(api, fieldextension.WithDegree(8)) + if err != nil { + return err + } + res := fe.Mul(commitment, commitment) + for i := range res { + api.AssertIsDifferent(res[i], 0) + } + return nil + }, 8, c.X) + return nil +} + +func TestWideCommitment(t *testing.T) { + f := koalabear.Modulus() + assert := test.NewAssert(t) + // should error as we call WithCommitment + err := test.IsSolved(&wideCommitment{withCommitment: true}, &wideCommitment{X: 10}, f) + assert.Error(err) + // should pass as we don't call WithCommitment + err = test.IsSolved(&wideCommitment{withCommitment: false}, &wideCommitment{X: 10}, f) + assert.NoError(err) + + // should fail as we don't have WithWideCommitment for r1cs and scs + _, err = frontend.Compile(f, r1cs.NewBuilder, &wideCommitment{withCommitment: false}) + assert.Error(err) + _, err = frontend.Compile(f, scs.NewBuilder, &wideCommitment{withCommitment: false}) + assert.Error(err) + + // should pass as we provide with builder with WideCommitment support + _, err = frontend.CompileU32(f, widecommitter.From(r1cs.NewBuilder), &wideCommitment{withCommitment: false}) + assert.NoError(err) + _, err = frontend.CompileU32(f, widecommitter.From(scs.NewBuilder), &wideCommitment{withCommitment: false}) + assert.NoError(err) + + // shouldn't pass if we have mixed WithCommitment and WithWideCommitment + _, err = frontend.CompileU32(f, widecommitter.From(scs.NewBuilder), &wideCommitment{withCommitment: true}) + assert.Error(err) + _, err = frontend.CompileU32(f, widecommitter.From(r1cs.NewBuilder), &wideCommitment{withCommitment: true}) + assert.Error(err) +} diff --git a/std/permutation/poseidon2/gkr-poseidon2/gkr.go b/std/permutation/poseidon2/gkr-poseidon2/gkr.go new file mode 100644 index 00000000..218d252e --- /dev/null +++ b/std/permutation/poseidon2/gkr-poseidon2/gkr.go @@ -0,0 +1,436 @@ +package gkr_poseidon2 + +import ( + "errors" + "fmt" + "math/big" + "sync" + + "github.com/consensys/gnark/constraint/solver" + "github.com/consensys/gnark/constraint/solver/gkrgates" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/internal/utils" + "github.com/consensys/gnark/std/gkrapi" + "github.com/consensys/gnark/std/gkrapi/gkr" + "github.com/consensys/gnark/std/hash" + _ "github.com/consensys/gnark/std/hash/mimc" // to ensure mimc is registered + + "github.com/consensys/gnark-crypto/ecc" + frBls12377 "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" + poseidon2Bls12377 "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/poseidon2" +) + +// extKeyGate applies the external matrix mul, then adds the round key +// because of its symmetry, we don't need to define distinct x1 and x2 versions of it +func extKeyGate(roundKey frontend.Variable) gkr.GateFunction { + return func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("expected 2 inputs") + } + return api.Add(api.Mul(x[0], 2), x[1], roundKey) + } +} + +// pow4Gate computes a -> a⁴ +func pow4Gate(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 1 { + panic("expected 1 input") + } + y := api.Mul(x[0], x[0]) + y = api.Mul(y, y) + + return y +} + +// pow4TimesGate computes a, b -> a⁴ * b +func pow4TimesGate(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("expected 1 input") + } + y := api.Mul(x[0], x[0]) + y = api.Mul(y, y) + + return api.Mul(y, x[1]) +} + +// pow2Gate computes a -> a² +func pow2Gate(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 1 { + panic("expected 1 input") + } + return api.Mul(x[0], x[0]) +} + +// pow2TimesGate computes a, b -> a² * b +func pow2TimesGate(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("expected 2 inputs") + } + return api.Mul(x[0], x[0], x[1]) +} + +// for x1, the partial round gates are identical to full round gates +// for x2, the partial round gates are just a linear combination +// TODO @Tabaie try eliminating the x2 partial round gates and have the x1 gates depend on i - rf/2 or so previous x1's + +// extGate2 applies the external matrix mul, outputting the second element of the result +func extGate2(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("expected 2 inputs") + } + return api.Add(api.Mul(x[1], 2), x[0]) +} + +// intKeyGate2 applies the internal matrix mul, then adds the round key +func intKeyGate2(roundKey frontend.Variable) gkr.GateFunction { + return func(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("expected 2 inputs") + } + return api.Add(api.Mul(x[1], 3), x[0], roundKey) + } +} + +// intGate2 applies the internal matrix mul. The round key is zero +func intGate2(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("expected 2 inputs") + } + return api.Add(api.Mul(x[1], 3), x[0]) +} + +// extGate applies the first row of the external matrix +func extGate(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 2 { + panic("expected 2 inputs") + } + return api.Add(api.Mul(x[0], 2), x[1]) +} + +// extAddGate applies the first row of the external matrix to the first two elements and adds the third +func extAddGate(api gkr.GateAPI, x ...frontend.Variable) frontend.Variable { + if len(x) != 3 { + panic("expected 3 inputs") + } + return api.Add(api.Mul(x[0], 2), x[1], x[2]) +} + +type GkrCompressions struct { + api frontend.API + ins1 []frontend.Variable + ins2 []frontend.Variable + outs []frontend.Variable +} + +// NewGkrCompressions returns an object that can compute the Poseidon2 compression function (currently only for BLS12-377) +// which consists of a permutation along with the input fed forward. +// The correctness of the compression functions is proven using GKR. +// Note that the solver will need the function RegisterGkrSolverOptions to be called with the desired curves +func NewGkrCompressions(api frontend.API) *GkrCompressions { + res := GkrCompressions{ + api: api, + } + api.Compiler().Defer(res.finalize) + return &res +} + +func (p *GkrCompressions) Compress(a, b frontend.Variable) frontend.Variable { + s, err := p.api.Compiler().NewHint(permuteHint, 1, a, b) + if err != nil { + panic(err) + } + p.ins1 = append(p.ins1, a) + p.ins2 = append(p.ins2, b) + p.outs = append(p.outs, s[0]) + return s[0] +} + +// defineCircuit defines the GKR circuit for the Poseidon2 permutation over BLS12-377 +// insLeft and insRight are the inputs to the permutation +// they must be padded to a power of 2 +func defineCircuit(insLeft, insRight []frontend.Variable) (*gkrapi.API, gkr.Variable, error) { + // variable indexes + const ( + xI = iota + yI + ) + + // poseidon2 parameters + gateNamer := newRoundGateNamer(poseidon2Bls12377.GetDefaultParameters()) + rF := poseidon2Bls12377.GetDefaultParameters().NbFullRounds + rP := poseidon2Bls12377.GetDefaultParameters().NbPartialRounds + halfRf := rF / 2 + + gkrApi := gkrapi.New() + + x, err := gkrApi.Import(insLeft) + if err != nil { + return nil, -1, err + } + y, err := gkrApi.Import(insRight) + y0 := y // save to feed forward at the end + if err != nil { + return nil, -1, err + } + + // *** helper functions to register and apply gates *** + + // Poseidon2 is a sequence of additions, exponentiations (s-Box), and linear operations + // but here we group the operations so that every round consists of a degree-1 operation followed by the s-Box + // this allows for more efficient result sharing among the gates + // but also breaks the uniformity of the circuit a bit, in that the matrix operation + // in every round comes from the previous (canonical) round. + + // apply the s-Box to u + // the s-Box gates: u¹⁷ = (u⁴)⁴ * u + sBox := func(u gkr.Variable) gkr.Variable { + v := gkrApi.Gate(pow4Gate, u) // u⁴ + return gkrApi.Gate(pow4TimesGate, v, u) // u¹⁷ + } + + // apply external matrix multiplication and round key addition + // round dependent due to the round key + extKeySBox := func(round, varI int, a, b gkr.Variable) gkr.Variable { + return sBox(gkrApi.NamedGate(gateNamer.linear(varI, round), a, b)) + } + + // apply external matrix multiplication and round key addition + // then apply the s-Box + // for the second variable + // round independent due to the round key + intKeySBox2 := func(round int, a, b gkr.Variable) gkr.Variable { + return sBox(gkrApi.NamedGate(gateNamer.linear(yI, round), a, b)) + } + + // apply a full round + fullRound := func(i int) { + x1 := extKeySBox(i, xI, x, y) + x, y = x1, extKeySBox(i, yI, y, x) // the external matrix is symmetric so we can use the same gate with inputs swapped + } + + // *** construct the circuit *** + + for i := range halfRf { + fullRound(i) + } + + { + // i = halfRf: first partial round + // still using the external matrix, since the linear operation still belongs to a full (canonical) round + x1 := extKeySBox(halfRf, xI, x, y) + + x, y = x1, gkrApi.Gate(extGate2, x, y) + } + + for i := halfRf + 1; i < halfRf+rP; i++ { + x1 := extKeySBox(i, xI, x, y) // the first row of the internal matrix is the same as that of the external matrix + x, y = x1, gkrApi.Gate(intGate2, x, y) + } + + { + i := halfRf + rP + // first iteration of the final batch of full rounds + // still using the internal matrix, since the linear operation still belongs to a partial (canonical) round + x1 := extKeySBox(i, xI, x, y) + x, y = x1, intKeySBox2(i, x, y) + } + + for i := halfRf + rP + 1; i < rP+rF; i++ { + fullRound(i) + } + + // apply the external matrix one last time to obtain the final value of y + y = gkrApi.NamedGate(gateNamer.linear(yI, rP+rF), y, x, y0) + + return gkrApi, y, nil +} + +func (p *GkrCompressions) finalize(api frontend.API) error { + if p.api != api { + panic("unexpected API") + } + + // register gates + registerGkrSolverOptions(api) + + // pad instances into a power of 2 + // TODO @Tabaie the GKR API to do this automatically? + ins1Padded := make([]frontend.Variable, ecc.NextPowerOfTwo(uint64(len(p.ins1)))) + ins2Padded := make([]frontend.Variable, len(ins1Padded)) + copy(ins1Padded, p.ins1) + copy(ins2Padded, p.ins2) + for i := len(p.ins1); i < len(ins1Padded); i++ { + ins1Padded[i] = 0 + ins2Padded[i] = 0 + } + + gkrApi, y, err := defineCircuit(ins1Padded, ins2Padded) + if err != nil { + return err + } + + // connect to output + // TODO can we save 1 constraint per instance by giving the desired outputs to the gkr api? + solution, err := gkrApi.Solve(api) + if err != nil { + return err + } + yVals := solution.Export(y) + for i := range p.outs { + api.AssertIsEqual(yVals[i], p.outs[i]) + } + + // verify GKR proof + allVals := make([]frontend.Variable, 0, 3*len(p.ins1)) + allVals = append(allVals, p.ins1...) + allVals = append(allVals, p.ins2...) + allVals = append(allVals, p.outs...) + challenge, err := p.api.(frontend.Committer).Commit(allVals...) + if err != nil { + return err + } + return solution.Verify(hash.MIMC.String(), challenge) +} + +// registerGkrSolverOptions is a wrapper for RegisterGkrSolverOptions +// that performs the registration for the curve associated with api. +func registerGkrSolverOptions(api frontend.API) { + RegisterGkrSolverOptions(utils.FieldToCurve(api.Compiler().Field())) +} + +func permuteHint(m *big.Int, ins, outs []*big.Int) error { + if m.Cmp(ecc.BLS12_377.ScalarField()) != 0 { + return errors.New("only bls12-377 supported") + } + if len(ins) != 2 || len(outs) != 1 { + return errors.New("expected 2 inputs and 1 output") + } + var x [2]frBls12377.Element + x[0].SetBigInt(ins[0]) + x[1].SetBigInt(ins[1]) + y0 := x[1] + + err := bls12377Permutation().Permutation(x[:]) + x[1].Add(&x[1], &y0) // feed forward + x[1].BigInt(outs[0]) + return err +} + +var bls12377Permutation = sync.OnceValue(func() *poseidon2Bls12377.Permutation { + params := poseidon2Bls12377.GetDefaultParameters() + return poseidon2Bls12377.NewPermutation(2, params.NbFullRounds, params.NbPartialRounds) // TODO @Tabaie add NewDefaultPermutation to gnark-crypto +}) + +// RegisterGkrSolverOptions registers the GKR gates corresponding to the given curves for the solver +func RegisterGkrSolverOptions(curves ...ecc.ID) { + if len(curves) == 0 { + panic("expected at least one curve") + } + solver.RegisterHint(permuteHint) + for _, curve := range curves { + switch curve { + case ecc.BLS12_377: + if err := registerGkrGatesBls12377(); err != nil { + panic(err) + } + default: + panic(fmt.Sprintf("curve %s not currently supported", curve)) + } + } +} + +func registerGkrGatesBls12377() error { + const ( + x = iota + y + ) + + p := poseidon2Bls12377.GetDefaultParameters() + halfRf := p.NbFullRounds / 2 + gateNames := newRoundGateNamer(p) + + if err := gkrgates.Register(pow2Gate, 1, gkrgates.WithUnverifiedDegree(2), gkrgates.WithNoSolvableVar()); err != nil { + return err + } + if err := gkrgates.Register(pow4Gate, 1, gkrgates.WithUnverifiedDegree(4), gkrgates.WithNoSolvableVar()); err != nil { + return err + } + if err := gkrgates.Register(pow2TimesGate, 2, gkrgates.WithUnverifiedDegree(3), gkrgates.WithNoSolvableVar()); err != nil { + return err + } + if err := gkrgates.Register(pow4TimesGate, 2, gkrgates.WithUnverifiedDegree(5), gkrgates.WithNoSolvableVar()); err != nil { + return err + } + + if err := gkrgates.Register(intGate2, 2, gkrgates.WithUnverifiedDegree(1), gkrgates.WithUnverifiedSolvableVar(0)); err != nil { + return err + } + + extKeySBox := func(round int, varIndex int) error { + return gkrgates.Register(extKeyGate(&p.RoundKeys[round][varIndex]), 2, gkrgates.WithUnverifiedDegree(1), gkrgates.WithUnverifiedSolvableVar(0), gkrgates.WithName(gateNames.linear(varIndex, round))) + } + + intKeySBox2 := func(round int) error { + return gkrgates.Register(intKeyGate2(&p.RoundKeys[round][1]), 2, gkrgates.WithUnverifiedDegree(1), gkrgates.WithUnverifiedSolvableVar(0), gkrgates.WithName(gateNames.linear(y, round))) + } + + fullRound := func(i int) error { + if err := extKeySBox(i, x); err != nil { + return err + } + return extKeySBox(i, y) + } + + for round := range halfRf { + if err := fullRound(round); err != nil { + return err + } + } + + { // round = halfRf: first partial one + if err := extKeySBox(halfRf, x); err != nil { + return err + } + } + + for round := halfRf + 1; round < halfRf+p.NbPartialRounds; round++ { + if err := extKeySBox(round, x); err != nil { // for x1, intKeySBox is identical to extKeySBox + return err + } + } + + { + round := halfRf + p.NbPartialRounds + if err := extKeySBox(round, x); err != nil { + return err + } + if err := intKeySBox2(round); err != nil { + return err + } + } + + for round := halfRf + p.NbPartialRounds + 1; round < p.NbPartialRounds+p.NbFullRounds; round++ { + if err := fullRound(round); err != nil { + return err + } + } + + return gkrgates.Register(extAddGate, 3, gkrgates.WithUnverifiedDegree(1), gkrgates.WithUnverifiedSolvableVar(0), gkrgates.WithName(gateNames.linear(y, p.NbPartialRounds+p.NbFullRounds))) +} + +type roundGateNamer string + +// newRoundGateNamer returns an object that returns standardized names for gates in the GKR circuit +func newRoundGateNamer(p fmt.Stringer) roundGateNamer { + return roundGateNamer(p.String()) +} + +// linear is the name of a gate where a polynomial of total degree 1 is applied to the input +func (n roundGateNamer) linear(varIndex, round int) gkr.GateName { + return gkr.GateName(fmt.Sprintf("x%d-l-op-round=%d;%s", varIndex, round, n)) +} + +// integrated is the name of a gate where a polynomial of total degree 1 is applied to the input, followed by an S-box +func (n roundGateNamer) integrated(varIndex, round int) gkr.GateName { + return gkr.GateName(fmt.Sprintf("x%d-i-op-round=%d;%s", varIndex, round, n)) +} diff --git a/std/permutation/poseidon2/gkr_test.go b/std/permutation/poseidon2/gkr-poseidon2/gkr_test.go similarity index 88% rename from std/permutation/poseidon2/gkr_test.go rename to std/permutation/poseidon2/gkr-poseidon2/gkr_test.go index 1cc39c40..1503054a 100644 --- a/std/permutation/poseidon2/gkr_test.go +++ b/std/permutation/poseidon2/gkr-poseidon2/gkr_test.go @@ -1,17 +1,18 @@ -package poseidon2 +package gkr_poseidon2 import ( "fmt" + "testing" + "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/scs" "github.com/consensys/gnark/test" "github.com/stretchr/testify/require" - "testing" ) -func TestGkrPermutation(t *testing.T) { +func TestGkrCompression(t *testing.T) { const n = 2 var k int64 ins := make([][2]frontend.Variable, n) @@ -22,8 +23,10 @@ func TestGkrPermutation(t *testing.T) { x[0].SetInt64(k) x[1].SetInt64(k + 1) + y0 := x[1] require.NoError(t, bls12377Permutation().Permutation(x[:])) + x[1].Add(&x[1], &y0) outs[i] = x[1] k += 2 @@ -46,10 +49,10 @@ type testGkrPermutationCircuit struct { func (c *testGkrPermutationCircuit) Define(api frontend.API) error { - pos2 := NewGkrPermutations(api) + pos2 := NewGkrCompressions(api) api.AssertIsEqual(len(c.Ins), len(c.Outs)) for i := range c.Ins { - api.AssertIsEqual(c.Outs[i], pos2.Permute(c.Ins[i][0], c.Ins[i][1])) + api.AssertIsEqual(c.Outs[i], pos2.Compress(c.Ins[i][0], c.Ins[i][1])) } return nil diff --git a/std/permutation/poseidon2/gkr.go b/std/permutation/poseidon2/gkr.go deleted file mode 100644 index f52d750b..00000000 --- a/std/permutation/poseidon2/gkr.go +++ /dev/null @@ -1,398 +0,0 @@ -package poseidon2 - -import ( - "errors" - "fmt" - "github.com/consensys/gnark/constraint/solver" - "hash" - "math/big" - "sync" - - "github.com/consensys/gnark-crypto/ecc" - frBls12377 "github.com/consensys/gnark-crypto/ecc/bls12-377/fr" - mimcBls12377 "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/mimc" - poseidon2Bls12377 "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/poseidon2" - gkrPoseidon2Bls12377 "github.com/consensys/gnark-crypto/ecc/bls12-377/fr/poseidon2/gkrgates" - "github.com/consensys/gnark/constraint" - csBls12377 "github.com/consensys/gnark/constraint/bls12-377" - "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/std/gkr" - stdHash "github.com/consensys/gnark/std/hash" - "github.com/consensys/gnark/std/hash/mimc" -) - -// extKeyGate applies the external matrix mul, then adds the round key -// because of its symmetry, we don't need to define distinct x1 and x2 versions of it -type extKeyGate struct { - roundKey *big.Int -} - -func (g *extKeyGate) Evaluate(api frontend.API, x ...frontend.Variable) frontend.Variable { - if len(x) != 2 { - panic("expected 2 inputs") - } - return api.Add(api.Mul(x[0], 2), x[1], g.roundKey) -} - -func (g *extKeyGate) Degree() int { - return 1 -} - -// pow4Gate computes a -> a⁴ -type pow4Gate struct{} - -func (g pow4Gate) Evaluate(api frontend.API, x ...frontend.Variable) frontend.Variable { - if len(x) != 1 { - panic("expected 1 input") - } - y := api.Mul(x[0], x[0]) - y = api.Mul(y, y) - - return y -} - -func (g pow4Gate) Degree() int { - return 4 -} - -// pow4Gate computes a, b -> a⁴ * b -type pow4TimesGate struct{} - -func (g pow4TimesGate) Evaluate(api frontend.API, x ...frontend.Variable) frontend.Variable { - if len(x) != 2 { - panic("expected 1 input") - } - y := api.Mul(x[0], x[0]) - y = api.Mul(y, y) - - return api.Mul(y, x[1]) -} - -func (g pow4TimesGate) Degree() int { - return 5 -} - -type pow2Gate struct{} - -func (g pow2Gate) Evaluate(api frontend.API, x ...frontend.Variable) frontend.Variable { - if len(x) != 1 { - panic("expected 1 input") - } - return api.Mul(x[0], x[0]) -} - -func (g pow2Gate) Degree() int { - return 2 -} - -type pow2TimesGate struct{} - -func (g pow2TimesGate) Evaluate(api frontend.API, x ...frontend.Variable) frontend.Variable { - if len(x) != 2 { - panic("expected 2 inputs") - } - return api.Mul(x[0], x[0], x[1]) -} - -func (g pow2TimesGate) Degree() int { - return 3 -} - -// for x1, the partial round gates are identical to full round gates -// for x2, the partial round gates are just a linear combination -// TODO @Tabaie try eliminating the x2 partial round gates and have the x1 gates depend on i - rf/2 or so previous x1's - -// extGate2 applies the external matrix mul, outputting the second element of the result -type extGate2 struct { -} - -func (g *extGate2) Evaluate(api frontend.API, x ...frontend.Variable) frontend.Variable { - if len(x) != 2 { - panic("expected 2 inputs") - } - return api.Add(api.Mul(x[1], 2), x[0]) -} - -func (g *extGate2) Degree() int { - return 1 -} - -// intKeyGate2 applies the internal matrix mul, then adds the round key -type intKeyGate2 struct { - roundKey *big.Int -} - -func (g *intKeyGate2) Evaluate(api frontend.API, x ...frontend.Variable) frontend.Variable { - if len(x) != 2 { - panic("expected 2 inputs") - } - return api.Add(api.Mul(x[1], 3), x[0], g.roundKey) -} - -func (g *intKeyGate2) Degree() int { - return 1 -} - -type extGate struct{} - -func (g extGate) Evaluate(api frontend.API, x ...frontend.Variable) frontend.Variable { - if len(x) != 2 { - panic("expected 2 inputs") - } - return api.Add(api.Mul(x[0], 2), x[1]) -} - -func (g extGate) Degree() int { - return 1 -} - -type GkrPermutations struct { - api frontend.API - ins1 []frontend.Variable - ins2 []frontend.Variable - outs []frontend.Variable -} - -// NewGkrPermutations returns an object that can compute the Poseidon2 permutation (currently only for BLS12-377) -// The correctness of the permutations is proven using GKR -// Note that the solver will need the function RegisterGkrSolverOptions to be called with the desired curves -func NewGkrPermutations(api frontend.API) *GkrPermutations { - res := GkrPermutations{ - api: api, - } - api.Compiler().Defer(res.finalize) - return &res -} - -func (p *GkrPermutations) Permute(a, b frontend.Variable) frontend.Variable { - s, err := p.api.Compiler().NewHint(permuteHint, 1, a, b) - if err != nil { - panic(err) - } - p.ins1 = append(p.ins1, a) - p.ins2 = append(p.ins2, b) - p.outs = append(p.outs, s[0]) - return s[0] -} - -func frToInt(x *frBls12377.Element) *big.Int { - var res big.Int - x.BigInt(&res) - return &res -} - -// defineCircuit defines the GKR circuit for the Poseidon2 permutation over BLS12-377 -// insLeft and insRight are the inputs to the permutation -// they must be padded to a power of 2 -func defineCircuit(insLeft, insRight []frontend.Variable) (*gkr.API, constraint.GkrVariable, error) { - // variable indexes - const ( - xI = iota - yI - ) - - // poseidon2 parameters - roundKeysFr := poseidon2Bls12377.GetDefaultParameters().RoundKeys - params := poseidon2Bls12377.GetDefaultParameters().String() - rF := poseidon2Bls12377.GetDefaultParameters().NbFullRounds - rP := poseidon2Bls12377.GetDefaultParameters().NbPartialRounds - halfRf := rF / 2 - - gkrApi := gkr.NewApi() - - x, err := gkrApi.Import(insLeft) - if err != nil { - return nil, -1, err - } - y, err := gkrApi.Import(insRight) - if err != nil { - return nil, -1, err - } - - // unique names for linear rounds - gateNameLinear := func(varI, round int) string { - return fmt.Sprintf("x%d-l-op-round=%d;%s", varI, round, params) - } - - // the s-Box gates: u¹⁷ = (u⁴)⁴ * u - gkr.Gates["pow4"] = pow4Gate{} - gkr.Gates["pow4Times"] = pow4TimesGate{} - - // *** helper functions to register and apply gates *** - - // Poseidon2 is a sequence of additions, exponentiations (s-Box), and linear operations - // but here we group the operations so that every round consists of a degree-1 operation followed by the s-Box - // this allows for more efficient result sharing among the gates - // but also breaks the uniformity of the circuit a bit, in that the matrix operation - // in every round comes from the previous (canonical) round. - - // apply the s-Box to u - sBox := func(u constraint.GkrVariable) constraint.GkrVariable { - v := gkrApi.NamedGate("pow4", u) // u⁴ - return gkrApi.NamedGate("pow4Times", v, u) // u¹⁷ - } - - // register and apply external matrix multiplication and round key addition - // round dependent due to the round key - extKeySBox := func(round, varI int, a, b constraint.GkrVariable) constraint.GkrVariable { - gate := gateNameLinear(varI, round) - gkr.Gates[gate] = &extKeyGate{ - roundKey: frToInt(&roundKeysFr[round][varI]), - } - return sBox(gkrApi.NamedGate(gate, a, b)) - } - - // register and apply external matrix multiplication and round key addition - // then apply the s-Box - // for the second variable - // round independent due to the round key - intKeySBox2 := func(round int, a, b constraint.GkrVariable) constraint.GkrVariable { - gate := gateNameLinear(yI, round) - gkr.Gates[gate] = &intKeyGate2{ - roundKey: frToInt(&roundKeysFr[round][1]), - } - return sBox(gkrApi.NamedGate(gate, a, b)) - } - - // apply a full round - fullRound := func(i int) { - x1 := extKeySBox(i, xI, x, y) // TODO inline this - x, y = x1, extKeySBox(i, yI, y, x) // the external matrix is symmetric so we can use the same gate with inputs swapped - } - - // *** construct the circuit *** - - for i := range halfRf { - fullRound(i) - } - - { - // i = halfRf: first partial round - // still using the external matrix, since the linear operation still belongs to a full (canonical) round - x1 := extKeySBox(halfRf, xI, x, y) - - gate := gateNameLinear(yI, halfRf) - gkr.Gates[gate] = &extGate2{} - x, y = x1, gkrApi.NamedGate(gate, x, y) - } - - zero := new(big.Int) - for i := halfRf + 1; i < halfRf+rP; i++ { - x1 := extKeySBox(i, xI, x, y) // the first row of the internal matrix is the same as that of the external matrix - - gate := gateNameLinear(yI, i) - gkr.Gates[gate] = &intKeyGate2{ - roundKey: zero, - } - x, y = x1, gkrApi.NamedGate(gate, x, y) - } - - { - i := halfRf + rP - // first iteration of the final batch of full rounds - // still using the internal matrix, since the linear operation still belongs to a partial (canonical) round - x1 := extKeySBox(i, xI, x, y) - x, y = x1, intKeySBox2(i, x, y) - } - - for i := halfRf + rP + 1; i < rP+rF; i++ { - fullRound(i) - } - - // apply the external matrix one last time to obtain the final value of y - gate := gateNameLinear(yI, rP+rF) - gkr.Gates[gate] = extGate{} - y = gkrApi.NamedGate(gate, y, x) - - return gkrApi, y, nil -} - -func (p *GkrPermutations) finalize(api frontend.API) error { - if p.api != api { - panic("unexpected API") - } - - // register MiMC to be used as a random oracle in the GKR proof - stdHash.Register("mimc", func(api frontend.API) (stdHash.FieldHasher, error) { - m, err := mimc.NewMiMC(api) - return &m, err - }) - - // pad instances into a power of 2 - // TODO @Tabaie the GKR API to do this automatically? - ins1Padded := make([]frontend.Variable, ecc.NextPowerOfTwo(uint64(len(p.ins1)))) - ins2Padded := make([]frontend.Variable, len(ins1Padded)) - copy(ins1Padded, p.ins1) - copy(ins2Padded, p.ins2) - for i := len(p.ins1); i < len(ins1Padded); i++ { - ins1Padded[i] = 0 - ins2Padded[i] = 0 - } - - gkrApi, y, err := defineCircuit(ins1Padded, ins2Padded) - if err != nil { - return err - } - - // connect to output - // TODO can we save 1 constraint per instance by giving the desired outputs to the gkr api? - solution, err := gkrApi.Solve(api) - if err != nil { - return err - } - yVals := solution.Export(y) - for i := range p.outs { - api.AssertIsEqual(yVals[i], p.outs[i]) - } - - // verify GKR proof - allVals := make([]frontend.Variable, 0, 3*len(p.ins1)) - allVals = append(allVals, p.ins1...) - allVals = append(allVals, p.ins2...) - allVals = append(allVals, p.outs...) - challenge, err := p.api.(frontend.Committer).Commit(allVals...) - if err != nil { - return err - } - return solution.Verify("mimc", challenge) -} - -func permuteHint(m *big.Int, ins, outs []*big.Int) error { - if m.Cmp(ecc.BLS12_377.ScalarField()) != 0 { - return errors.New("only bls12-377 supported") - } - if len(ins) != 2 || len(outs) != 1 { - return errors.New("expected 2 inputs and 1 output") - } - var x [2]frBls12377.Element - x[0].SetBigInt(ins[0]) - x[1].SetBigInt(ins[1]) - - err := bls12377Permutation().Permutation(x[:]) - x[1].BigInt(outs[0]) - return err -} - -var bls12377Permutation = sync.OnceValue(func() *poseidon2Bls12377.Permutation { - params := poseidon2Bls12377.GetDefaultParameters() - return poseidon2Bls12377.NewPermutation(2, params.NbFullRounds, params.NbPartialRounds) // TODO @Tabaie add NewDefaultPermutation to gnark-crypto -}) - -// RegisterGkrSolverOptions registers the GKR gates corresponding to the given curves for the solver -func RegisterGkrSolverOptions(curves ...ecc.ID) { - if len(curves) == 0 { - panic("expected at least one curve") - } - solver.RegisterHint(permuteHint) - for _, curve := range curves { - switch curve { - case ecc.BLS12_377: - csBls12377.RegisterHashBuilder("mimc", func() hash.Hash { - return mimcBls12377.NewMiMC() - }) - gkrPoseidon2Bls12377.RegisterGkrGates() - default: - panic(fmt.Sprintf("curve %s not currently supported", curve)) - } - } -} diff --git a/std/permutation/poseidon2/poseidon2.go b/std/permutation/poseidon2/poseidon2.go index f7a8d406..55afe73b 100644 --- a/std/permutation/poseidon2/poseidon2.go +++ b/std/permutation/poseidon2/poseidon2.go @@ -328,5 +328,5 @@ func (h *Permutation) Compress(left, right frontend.Variable) frontend.Variable if err := h.Permutation(vars[:]); err != nil { panic(err) // this would never happen } - return vars[1] + return h.api.Add(vars[1], right) } diff --git a/std/polynomial/polynomial_test.go b/std/polynomial/polynomial_test.go index 667825a6..1bdf5dc3 100644 --- a/std/polynomial/polynomial_test.go +++ b/std/polynomial/polynomial_test.go @@ -1,15 +1,14 @@ -package polynomial +package polynomial_test import ( - "errors" "fmt" - "github.com/stretchr/testify/assert" "testing" "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/r1cs" "github.com/consensys/gnark/frontend/cs/scs" + "github.com/consensys/gnark/std/polynomial" "github.com/consensys/gnark/test" ) @@ -20,7 +19,7 @@ type evalPolyCircuit struct { } func (c *evalPolyCircuit) Define(api frontend.API) error { - p := Polynomial(c.P) + p := polynomial.Polynomial(c.P) evaluation := p.Eval(api, c.At) api.AssertIsEqual(evaluation, c.Evaluation) return nil @@ -30,74 +29,18 @@ func testEvalPoly(t *testing.T, p []int64, at int64, evaluation int64) { assert := test.NewAssert(t) witness := evalPolyCircuit{ - P: Polynomial(int64SliceToVariableSlice(p)), + P: polynomial.Polynomial(int64SliceToVariableSlice(p)), At: at, Evaluation: evaluation, } - assert.CheckCircuit(&evalPolyCircuit{P: make(Polynomial, len(p))}, test.WithValidAssignment(&witness)) + assert.CheckCircuit(&evalPolyCircuit{P: make(polynomial.Polynomial, len(p))}, test.WithValidAssignment(&witness)) } func TestEvalPoly(t *testing.T) { testEvalPoly(t, []int64{1, 2, 3, 4}, 5, 586) } -type evalDeltasCircuit struct { - ExpectedDeltas []frontend.Variable - At frontend.Variable -} - -func (c *evalDeltasCircuit) Define(api frontend.API) error { - observedDeltas := computeDeltaAtNaive(api, c.At, len(c.ExpectedDeltas)) - for i := range c.ExpectedDeltas { - api.AssertIsEqual(observedDeltas[i], c.ExpectedDeltas[i]) - } - return nil -} - -func testEvalDeltas(t *testing.T, at int64, expected []int64) { - - test.NewAssert(t).CheckCircuit( - &evalDeltasCircuit{ExpectedDeltas: make([]frontend.Variable, len(expected))}, - - test.WithValidAssignment(&evalDeltasCircuit{ExpectedDeltas: int64SliceToVariableSlice(expected), At: at}), - ) -} - -func TestEvalDeltasLinear(t *testing.T) { - testEvalDeltas(t, 2, []int64{-1, 2}) -} - -func TestEvalDeltasQuadratic(t *testing.T) { - testEvalDeltas(t, 3, []int64{1, -3, 3}) -} - -type foldMultiLinCircuit struct { - M []frontend.Variable - At frontend.Variable - Result []frontend.Variable -} - -func (c *foldMultiLinCircuit) Define(api frontend.API) error { - if len(c.M) != 2*len(c.Result) { - return errors.New("folding size mismatch") - } - m := MultiLin(c.M) - m.fold(api, c.At) - for i := range c.Result { - api.AssertIsEqual(m[i], c.Result[i]) - } - return nil -} - -func TestFoldSmall(t *testing.T) { - test.NewAssert(t).CheckCircuit( - &foldMultiLinCircuit{M: make([]frontend.Variable, 4), Result: make([]frontend.Variable, 2)}, - - test.WithValidAssignment(&foldMultiLinCircuit{M: []frontend.Variable{0, 1, 2, 3}, At: 2, Result: []frontend.Variable{4, 5}}), - ) -} - type evalMultiLinCircuit struct { M []frontend.Variable `gnark:",public"` At []frontend.Variable `gnark:",secret"` @@ -105,7 +48,7 @@ type evalMultiLinCircuit struct { } func (c *evalMultiLinCircuit) Define(api frontend.API) error { - m := MultiLin(c.M) + m := polynomial.MultiLin(c.M) evaluation := m.Evaluate(api, c.At) api.AssertIsEqual(evaluation, c.Evaluation) return nil @@ -116,12 +59,12 @@ func TestEvalMultiLin(t *testing.T) { // M = 2 X₀ + X₁ + 1 witness := evalMultiLinCircuit{ - M: MultiLin{1, 2, 3, 4}, + M: polynomial.MultiLin{1, 2, 3, 4}, At: []frontend.Variable{5, 6}, Evaluation: 17, } - assert.CheckCircuit(&evalMultiLinCircuit{M: make(MultiLin, 4), At: make([]frontend.Variable, 2)}, test.WithValidAssignment(&witness)) + assert.CheckCircuit(&evalMultiLinCircuit{M: make(polynomial.MultiLin, 4), At: make([]frontend.Variable, 2)}, test.WithValidAssignment(&witness)) } type evalEqCircuit struct { @@ -131,7 +74,7 @@ type evalEqCircuit struct { } func (c *evalEqCircuit) Define(api frontend.API) error { - evaluation := EvalEq(api, c.X, c.Y) + evaluation := polynomial.EvalEq(api, c.X, c.Y) api.AssertIsEqual(evaluation, c.Eq) return nil } @@ -155,7 +98,7 @@ type interpolateLDECircuit struct { } func (c *interpolateLDECircuit) Define(api frontend.API) error { - evaluation := InterpolateLDE(api, c.At, c.Values) + evaluation := polynomial.InterpolateLDE(api, c.At, c.Values) api.AssertIsEqual(evaluation, c.ExpectedInterpolation) return nil } @@ -216,12 +159,6 @@ func TestInterpolateQuadraticExtension(t *testing.T) { ) } -func TestNegFactorial(t *testing.T) { - for n, expected := range []int{0, -1, 2, -6, 24} { - assert.Equal(t, expected, negFactorial(n)) - } -} - func int64SliceToVariableSlice(slice []int64) []frontend.Variable { res := make([]frontend.Variable, 0, len(slice)) for _, v := range slice { @@ -233,8 +170,8 @@ func int64SliceToVariableSlice(slice []int64) []frontend.Variable { func ExampleMultiLin_Evaluate() { const logSize = 20 const size = 1 << logSize - m := MultiLin(make([]frontend.Variable, size)) - e := MultiLin(make([]frontend.Variable, logSize)) + m := polynomial.MultiLin(make([]frontend.Variable, size)) + e := polynomial.MultiLin(make([]frontend.Variable, logSize)) cs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &evalMultiLinCircuit{M: m, At: e, Evaluation: 0}) if err != nil { diff --git a/std/rangecheck/rangecheck.go b/std/rangecheck/rangecheck.go index 8aac734d..a1be194c 100644 --- a/std/rangecheck/rangecheck.go +++ b/std/rangecheck/rangecheck.go @@ -2,7 +2,7 @@ // // This package chooses the most optimal path for performing range checks: // - if the backend supports native range checking and the frontend exports the variables in the proprietary format by implementing [frontend.Rangechecker], then use it directly; -// - if the backend supports creating a commitment of variables by implementing [frontend.Committer], then we use the log-derivative variant [[Haböck22]] of the product argument as in [[BCG+18]] . [r1cs.NewBuilder] returns a builder which implements this interface; +// - if the backend supports creating a commitment of variables by implementing [frontend.Committer], then we use the log-derivative variant [[Haböck22]] of the product argument as in [[BCG+18]]. // - lacking these, we perform binary decomposition of variable into bits. // // [BCG+18]: https://eprint.iacr.org/2018/380 @@ -12,14 +12,8 @@ package rangecheck import ( "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/frontend/cs/r1cs" ) -// only for documentation purposes. If we import the package then godoc knows -// how to refer to package r1cs and we get nice links in godoc. We import the -// package anyway in test. -var _ = r1cs.NewBuilder - // New returns a new range checker depending on the frontend capabilities. func New(api frontend.API) frontend.Rangechecker { if rc, ok := api.(frontend.Rangechecker); ok { @@ -28,6 +22,16 @@ func New(api frontend.API) frontend.Rangechecker { if _, ok := api.(frontend.Committer); ok { return newCommitRangechecker(api) } + if _, ok := api.(frontend.WideCommitter); ok { + // native field extension package does not support inversion for now which is required + // for the logderivate argument. However, we use wide committer only for small fields + // where the backend already knows how to range check (and should implement Rangechecker interface). + // So we can just panic here to detect the case when the backend does not implement + // the range checker interface. + // + // See https://github.com/Consensys/gnark/pull/1493 + panic("wide committer does not support operations for range checking") + } return plainChecker{api: api} } diff --git a/std/recursion/groth16/verifier.go b/std/recursion/groth16/verifier.go index 77ace907..ae8f599e 100644 --- a/std/recursion/groth16/verifier.go +++ b/std/recursion/groth16/verifier.go @@ -30,7 +30,6 @@ import ( "github.com/consensys/gnark/std/algebra/native/sw_bls24315" "github.com/consensys/gnark/std/commitments/pedersen" "github.com/consensys/gnark/std/math/emulated" - "github.com/consensys/gnark/std/math/emulated/emparams" "github.com/consensys/gnark/std/recursion" ) @@ -219,6 +218,7 @@ func ValueOfVerifyingKey[G1El algebra.G1ElementT, G2El algebra.G2ElementT, GtEl return ret, fmt.Errorf("commitment key[%d]: %w", i, err) } } + ret.PublicAndCommitmentCommitted = tVk.PublicAndCommitmentCommitted case *VerifyingKey[sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT]: tVk, ok := vk.(*groth16backend_bls12377.VerifyingKey) if !ok { @@ -246,6 +246,7 @@ func ValueOfVerifyingKey[G1El algebra.G1ElementT, G2El algebra.G2ElementT, GtEl return ret, fmt.Errorf("commitment key[%d]: %w", i, err) } } + ret.PublicAndCommitmentCommitted = tVk.PublicAndCommitmentCommitted case *VerifyingKey[sw_bls12381.G1Affine, sw_bls12381.G2Affine, sw_bls12381.GTEl]: tVk, ok := vk.(*groth16backend_bls12381.VerifyingKey) if !ok { @@ -273,6 +274,7 @@ func ValueOfVerifyingKey[G1El algebra.G1ElementT, G2El algebra.G2ElementT, GtEl return ret, fmt.Errorf("commitment key[%d]: %w", i, err) } } + ret.PublicAndCommitmentCommitted = tVk.PublicAndCommitmentCommitted case *VerifyingKey[sw_bls24315.G1Affine, sw_bls24315.G2Affine, sw_bls24315.GT]: tVk, ok := vk.(*groth16backend_bls24315.VerifyingKey) if !ok { @@ -300,6 +302,7 @@ func ValueOfVerifyingKey[G1El algebra.G1ElementT, G2El algebra.G2ElementT, GtEl return ret, fmt.Errorf("commitment key[%d]: %w", i, err) } } + ret.PublicAndCommitmentCommitted = tVk.PublicAndCommitmentCommitted case *VerifyingKey[sw_bw6761.G1Affine, sw_bw6761.G2Affine, sw_bw6761.GTEl]: tVk, ok := vk.(*groth16backend_bw6761.VerifyingKey) if !ok { @@ -327,6 +330,7 @@ func ValueOfVerifyingKey[G1El algebra.G1ElementT, G2El algebra.G2ElementT, GtEl return ret, fmt.Errorf("commitment key[%d]: %w", i, err) } } + ret.PublicAndCommitmentCommitted = tVk.PublicAndCommitmentCommitted default: return ret, fmt.Errorf("unknown parametric type combination") } @@ -521,7 +525,7 @@ func ValueOfWitness[FR emulated.FieldParams](w witness.Witness) (Witness[FR], er return ret, fmt.Errorf("expected fr_bn254.Vector, got %T", vec) } for i := range vect { - s.Public = append(s.Public, emulated.ValueOf[emparams.BN254Fr](vect[i])) + s.Public = append(s.Public, sw_bn254.NewScalar(vect[i])) } case *Witness[sw_bls12377.ScalarField]: vect, ok := vec.(fr_bls12377.Vector) @@ -537,7 +541,7 @@ func ValueOfWitness[FR emulated.FieldParams](w witness.Witness) (Witness[FR], er return ret, fmt.Errorf("expected fr_bls12381.Vector, got %T", vec) } for i := range vect { - s.Public = append(s.Public, emulated.ValueOf[emparams.BLS12381Fr](vect[i])) + s.Public = append(s.Public, sw_bls12381.NewScalar(vect[i])) } case *Witness[sw_bls24315.ScalarField]: vect, ok := vec.(fr_bls24315.Vector) @@ -687,3 +691,86 @@ func (v *Verifier[FR, G1El, G2El, GtEl]) AssertProof(vk VerifyingKey[G1El, G2El, v.pairing.AssertIsEqual(pairing, &vk.E) return nil } + +// SwitchVerification key switches the verification key based on the provided +// index idx. Can be used for recursive verification based on the verification +// key index. +func (v *Verifier[FR, G1El, G2El, GtEl]) SwitchVerificationKey(idx frontend.Variable, vks []VerifyingKey[G1El, G2El, GtEl]) (VerifyingKey[G1El, G2El, GtEl], error) { + var ret VerifyingKey[G1El, G2El, GtEl] + if len(vks) == 0 { + return ret, fmt.Errorf("no verifying keys provided") + } + if len(vks) == 1 { + v.api.AssertIsEqual(idx, 0) + return vks[0], nil + } + // commitment info + for i := 1; i < len(vks); i++ { + if len(vks[i].PublicAndCommitmentCommitted) != len(vks[0].PublicAndCommitmentCommitted) { + return ret, fmt.Errorf("invalid number of commitments") + } + for j := range vks[i].PublicAndCommitmentCommitted { + if len(vks[i].PublicAndCommitmentCommitted[j]) != len(vks[0].PublicAndCommitmentCommitted[j]) { + return ret, fmt.Errorf("invalid number of public committed variables") + } + for k := range vks[i].PublicAndCommitmentCommitted[j] { + if vks[i].PublicAndCommitmentCommitted[j][k] != vks[0].PublicAndCommitmentCommitted[j][k] { + return ret, fmt.Errorf("invalid public committed variable index") + } + } + } + if len(vks[i].CommitmentKeys) != len(vks[0].CommitmentKeys) { + return ret, fmt.Errorf("invalid number of commitment keys") + } + } + ret.PublicAndCommitmentCommitted = make([][]int, len(vks[0].PublicAndCommitmentCommitted)) + for i := range vks[0].PublicAndCommitmentCommitted { + ret.PublicAndCommitmentCommitted[i] = make([]int, len(vks[0].PublicAndCommitmentCommitted[i])) + copy(ret.PublicAndCommitmentCommitted[i], vks[0].PublicAndCommitmentCommitted[i]) + } + + ret.CommitmentKeys = make([]pedersen.VerifyingKey[G2El], len(vks[0].CommitmentKeys)) + for i := range ret.CommitmentKeys { + cmtBss := make([]*G2El, len(vks)) + cmtBexs := make([]*G2El, len(vks)) + for j := range vks { + cmtBss[j] = &vks[j].CommitmentKeys[i].G + cmtBexs[j] = &vks[j].CommitmentKeys[i].GSigmaNeg + } + ret.CommitmentKeys[i].G = *v.pairing.MuxG2(idx, cmtBss...) + ret.CommitmentKeys[i].GSigmaNeg = *v.pairing.MuxG2(idx, cmtBexs...) + } + // switch E + Es := make([]*GtEl, len(vks)) + for i := range vks { + Es[i] = &vks[i].E + } + ret.E = *v.pairing.MuxGt(idx, Es...) + + // Switch K + for i := 1; i < len(vks); i++ { + if len(vks[i].G1.K) != len(vks[0].G1.K) { + return ret, fmt.Errorf("invalid number of K elements") + } + } + ret.G1.K = make([]G1El, len(vks[0].G1.K)) + for i := range ret.G1.K { + Ks := make([]*G1El, len(vks)) + for j := range vks { + Ks[j] = &vks[j].G1.K[i] + } + ret.G1.K[i] = *v.curve.Mux(idx, Ks...) + } + + // Switch G2 + gammaNegs := make([]*G2El, len(vks)) + deltaNegs := make([]*G2El, len(vks)) + for i := range vks { + gammaNegs[i] = &vks[i].G2.GammaNeg + deltaNegs[i] = &vks[i].G2.DeltaNeg + } + ret.G2.GammaNeg = *v.pairing.MuxG2(idx, gammaNegs...) + ret.G2.DeltaNeg = *v.pairing.MuxG2(idx, deltaNegs...) + + return ret, nil +} diff --git a/std/recursion/groth16/verifier_test.go b/std/recursion/groth16/verifier_test.go index 3a4f40ac..72e8e850 100644 --- a/std/recursion/groth16/verifier_test.go +++ b/std/recursion/groth16/verifier_test.go @@ -1,6 +1,7 @@ package groth16 import ( + "crypto/rand" "fmt" "math/big" "testing" @@ -338,11 +339,11 @@ func (c *InnerCircuitCommitment) Define(api frontend.API) error { res := api.Mul(c.P, c.Q) api.AssertIsEqual(res, c.N) - commitment, err := api.Compiler().(frontend.Committer).Commit(c.P, c.Q, c.N) + // commitment both to internal and public + commitment, err := api.Compiler().(frontend.Committer).Commit(res, c.N) if err != nil { return err } - api.AssertIsDifferent(commitment, 0) return nil @@ -453,3 +454,145 @@ func TestBW6InBN254Commitment(t *testing.T) { err = test.IsSolved(outerCircuit, outerAssignment, ecc.BN254.ScalarField()) assert.NoError(err) } + +type innerParametricCircuit struct { + nbConstraints int + SecretInput frontend.Variable `gnark:",secret"` + PublicInputs frontend.Variable `gnark:",public"` +} + +func (c *innerParametricCircuit) Define(api frontend.API) error { + res := api.Mul(c.SecretInput, c.SecretInput) + for i := 2; i < c.nbConstraints-1; i++ { + res = api.Mul(res, c.SecretInput) + } + api.AssertIsEqual(c.PublicInputs, res) + commitment, err := api.Compiler().(frontend.Committer).Commit(res, c.PublicInputs) + if err != nil { + return err + } + + api.AssertIsDifferent(commitment, 0) + return nil +} + +// getInnerParametric method returns a dummy circuit with the number of constraints +// of the main one provided as argument, it also generates a proof for this +// circuit and verifies it. It returns the circuit, the verifying key, the +// public witness and the proof. +func getInnerParametric(assert *test.Assert, nbConstraints int, field, outer *big.Int) ( + constraint.ConstraintSystem, groth16.VerifyingKey, witness.Witness, groth16.Proof, +) { + dummyCcs, err := frontend.Compile(field, r1cs.NewBuilder, &innerParametricCircuit{ + nbConstraints: nbConstraints, + }) + assert.NoError(err) + dummyPK, dummyVK, err := groth16.Setup(dummyCcs) + assert.NoError(err) + + // dummy proof + x, err := rand.Int(rand.Reader, field) + assert.NoError(err) + res := big.NewInt(1) + for i := 0; i < nbConstraints-1; i++ { + res.Mul(res, x) + } + dummyAssignment := &innerParametricCircuit{ + SecretInput: x, + PublicInputs: res, + } + dummyWitness, err := frontend.NewWitness(dummyAssignment, field) + assert.NoError(err) + dummyProof, err := groth16.Prove(dummyCcs, dummyPK, dummyWitness, GetNativeProverOptions(outer, field)) + assert.NoError(err) + dummyPubWitness, err := dummyWitness.Public() + assert.NoError(err) + err = groth16.Verify(dummyProof, dummyVK, dummyPubWitness, GetNativeVerifierOptions(outer, field)) + assert.NoError(err) + return dummyCcs, dummyVK, dummyPubWitness, dummyProof +} + +type OuterCircuitMulti[FR emulated.FieldParams, G1El algebra.G1ElementT, G2El algebra.G2ElementT, GtEl algebra.GtElementT] struct { + // selectors include a 1 for inner and 0 for dummy verification keys + // it allows to switch between the two vks to use the right one for each + // proof and witness + Selectors []frontend.Variable + Proofs []Proof[G1El, G2El] + // vks includes the dummy vk in the first place and the inner vk in the + // second place + vks []VerifyingKey[G1El, G2El, GtEl] `gnark:"-"` + InnerWitnesses []Witness[FR] `gnark:",public"` +} + +func (c *OuterCircuitMulti[FR, G1El, G2El, GtEl]) Define(api frontend.API) error { + // init the verifier + verifier, err := NewVerifier[FR, G1El, G2El, GtEl](api) + if err != nil { + return fmt.Errorf("new verifier: %w", err) + } + // switch between vkeys based on each selector + for i, selector := range c.Selectors { + vk, err := verifier.SwitchVerificationKey(selector, c.vks) + if err != nil { + return fmt.Errorf("switch vk: %w", err) + } + if err := verifier.AssertProof(vk, c.Proofs[i], c.InnerWitnesses[i]); err != nil { + return err + } + } + return nil +} + +func TestBLS12InBW6Multi(t *testing.T) { + innertField := ecc.BLS12_377.ScalarField() + outerField := ecc.BW6_761.ScalarField() + nbCircuit := 5 + nbProofs := 5 + assert := test.NewAssert(t) + var err error + + ccss := make([]constraint.ConstraintSystem, nbCircuit) + vks := make([]groth16.VerifyingKey, nbCircuit) + witnesses := make([]witness.Witness, nbCircuit) + proofs := make([]groth16.Proof, nbCircuit) + for i := 0; i < nbCircuit; i++ { + // the different circuits can have different sizes. However, the number of public inputs and commitments must match + ccss[i], vks[i], witnesses[i], proofs[i] = getInnerParametric(assert, 100*(i+1), innertField, outerField) + } + circuitVks := make([]VerifyingKey[sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT], nbCircuit) + for i, vk := range vks { + circuitVks[i], err = ValueOfVerifyingKey[sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT](vk) + assert.NoError(err) + } + circuitProofs := make([]Proof[sw_bls12377.G1Affine, sw_bls12377.G2Affine], nbProofs) + circuitWitnesses := make([]Witness[sw_bls12377.ScalarField], nbProofs) + innerSelectors := make([]int, nbProofs) + circuitSelectors := make([]frontend.Variable, nbProofs) + for i := 0; i < nbProofs; i++ { + selector, err := rand.Int(rand.Reader, big.NewInt(int64(nbCircuit))) + assert.NoError(err) + innerSelectors[i] = int(selector.Int64()) + circuitSelectors[i] = frontend.Variable(innerSelectors[i]) + circuitProofs[i], err = ValueOfProof[sw_bls12377.G1Affine, sw_bls12377.G2Affine](proofs[innerSelectors[i]]) + assert.NoError(err) + circuitWitnesses[i], err = ValueOfWitness[sw_bls12377.ScalarField](witnesses[innerSelectors[i]]) + assert.NoError(err) + } + outerCircuit := &OuterCircuitMulti[sw_bls12377.ScalarField, sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT]{ + Selectors: make([]frontend.Variable, nbProofs), + Proofs: make([]Proof[sw_bls12377.G1Affine, sw_bls12377.G2Affine], nbProofs), + InnerWitnesses: make([]Witness[sw_bls12377.ScalarField], nbProofs), + vks: circuitVks, // the inner verification keys are hardcoded in the aggregation circuit + } + for i := 0; i < nbProofs; i++ { + outerCircuit.Proofs[i] = PlaceholderProof[sw_bls12377.G1Affine, sw_bls12377.G2Affine](ccss[0]) + outerCircuit.InnerWitnesses[i] = PlaceholderWitness[sw_bls12377.ScalarField](ccss[0]) + } + outerAssignment := &OuterCircuitMulti[sw_bls12377.ScalarField, sw_bls12377.G1Affine, sw_bls12377.G2Affine, sw_bls12377.GT]{ + Selectors: circuitSelectors, + InnerWitnesses: circuitWitnesses, + Proofs: circuitProofs, + } + err = test.IsSolved(outerCircuit, outerAssignment, ecc.BW6_761.ScalarField()) + assert.NoError(err) +} diff --git a/std/recursion/plonk/verifier.go b/std/recursion/plonk/verifier.go index cec5b15e..688866cb 100644 --- a/std/recursion/plonk/verifier.go +++ b/std/recursion/plonk/verifier.go @@ -1221,6 +1221,8 @@ func (v *Verifier[FR, G1El, G2El, GtEl]) SwitchVerificationKey(bvk BaseVerifying return ret, fmt.Errorf("no circuit verification keys given") } if len(cvks) == 1 { + // we don't need to switch. But the index needs to be 0. + v.api.AssertIsEqual(idx, 0) return VerifyingKey[FR, G1El, G2El]{ BaseVerifyingKey: bvk, CircuitVerifyingKey: cvks[0], diff --git a/std/recursion/sumcheck/prover.go b/std/recursion/sumcheck/prover.go index c075cf15..1dcaafa2 100644 --- a/std/recursion/sumcheck/prover.go +++ b/std/recursion/sumcheck/prover.go @@ -62,7 +62,7 @@ func prove(current *big.Int, target *big.Int, claims claims, opts ...proverOptio // defines the number of rounds. nbVars := claims.NbVars() proof.RoundPolyEvaluations = make([]nativePolynomial, nbVars) - // the first round in the sumcheck is without verifier challenge. Combine challenges and provers sends the first polynomial + // the first round in the sumcheck is without verifier challenge. combine challenges and provers sends the first polynomial proof.RoundPolyEvaluations[0] = claims.Combine(combinationCoef) challenges := make([]*big.Int, nbVars) diff --git a/std/selector/mux.go b/std/selector/mux.go index d4374303..32909ce6 100644 --- a/std/selector/mux.go +++ b/std/selector/mux.go @@ -2,6 +2,7 @@ package selector import ( "fmt" + "github.com/consensys/gnark/frontend" ) diff --git a/std/selector/mux_test.go b/std/selector/mux_test.go index f590c23f..b4ae7da3 100644 --- a/std/selector/mux_test.go +++ b/std/selector/mux_test.go @@ -1,9 +1,10 @@ package selector import ( + "testing" + "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/test" - "testing" ) type binaryMuxCircuit struct { diff --git a/std/selector/slice.go b/std/selector/slice.go index b21be605..5fd0018e 100644 --- a/std/selector/slice.go +++ b/std/selector/slice.go @@ -2,8 +2,9 @@ package selector import ( "fmt" - "github.com/consensys/gnark/frontend" "math/big" + + "github.com/consensys/gnark/frontend" ) // Slice selects a slice of the input array at indices [start, end), and zeroes the array at other diff --git a/std/selector/slice_test.go b/std/selector/slice_test.go index 35715c88..4e76cabc 100644 --- a/std/selector/slice_test.go +++ b/std/selector/slice_test.go @@ -1,10 +1,11 @@ package selector_test import ( + "testing" + "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/std/selector" "github.com/consensys/gnark/test" - "testing" ) type partitionerCircuit struct { diff --git a/test/api_assertions_test.go b/test/api_assertions_test.go index f53e2e2f..a445c5e1 100644 --- a/test/api_assertions_test.go +++ b/test/api_assertions_test.go @@ -1,9 +1,10 @@ package test import ( - "github.com/consensys/gnark/frontend" "math/rand" "testing" + + "github.com/consensys/gnark/frontend" ) func TestIsCrumb(t *testing.T) { diff --git a/test/assert.go b/test/assert.go index 358abf93..8803205c 100644 --- a/test/assert.go +++ b/test/assert.go @@ -6,6 +6,7 @@ package test import ( "errors" "fmt" + "math/big" "reflect" "strings" "testing" @@ -100,10 +101,10 @@ func (assert *Assert) SolvingFailed(circuit frontend.Circuit, invalidWitness fro assert.CheckCircuit(circuit, newOpts...) } -func lazySchema(circuit frontend.Circuit) func() *schema.Schema { +func lazySchema(field *big.Int, circuit frontend.Circuit) func() *schema.Schema { return func() *schema.Schema { // we only parse the schema if we need to display the witness in json. - s, err := schema.New(circuit, tVariable) + s, err := schema.New(field, circuit, tVariable) if err != nil { panic("couldn't parse schema from circuit: " + err.Error()) } @@ -144,13 +145,13 @@ func (assert *Assert) compile(circuit frontend.Circuit, curveID ecc.ID, backendI // error ensure the error is set, else fails the test // add a witness to the error message if provided -func (assert *Assert) error(err error, w *_witness) { +func (assert *Assert) error(field *big.Int, err error, w *_witness) { if err != nil { return } json := "" if w != nil { - bjson, err := w.full.ToJSON(lazySchema(w.assignment)()) + bjson, err := w.full.ToJSON(lazySchema(field, w.assignment)()) if err != nil { json = err.Error() } else { @@ -164,7 +165,7 @@ func (assert *Assert) error(err error, w *_witness) { // ensure the error is nil, else fails the test // add a witness to the error message if provided -func (assert *Assert) noError(err error, w *_witness) { +func (assert *Assert) noError(field *big.Int, err error, w *_witness) { if err == nil { return } @@ -173,7 +174,7 @@ func (assert *Assert) noError(err error, w *_witness) { if w != nil { var json string - bjson, err := w.full.ToJSON(lazySchema(w.assignment)()) + bjson, err := w.full.ToJSON(lazySchema(field, w.assignment)()) if err != nil { json = err.Error() } else { diff --git a/test/assert_checkcircuit.go b/test/assert_checkcircuit.go index 15789da5..ba44ab6b 100644 --- a/test/assert_checkcircuit.go +++ b/test/assert_checkcircuit.go @@ -50,7 +50,7 @@ func (assert *Assert) CheckCircuit(circuit frontend.Circuit, opts ...TestingOpti // check that the assignment is valid with the test engine if !opt.skipTestEngine { err := IsSolved(circuit, w.assignment, curve.ScalarField()) - assert.noError(err, &w) + assert.noError(curve.ScalarField(), err, &w) } } @@ -61,7 +61,7 @@ func (assert *Assert) CheckCircuit(circuit frontend.Circuit, opts ...TestingOpti // check that the assignment is invalid with the test engine if !opt.skipTestEngine { err := IsSolved(circuit, w.assignment, curve.ScalarField()) - assert.error(err, &w) + assert.error(curve.ScalarField(), err, &w) } } @@ -74,7 +74,7 @@ func (assert *Assert) CheckCircuit(circuit frontend.Circuit, opts ...TestingOpti // 1- check that the circuit compiles ccs, err := assert.compile(circuit, curve, b, opt.compileOpts) - assert.noError(err, nil) + assert.noError(curve.ScalarField(), err, nil) // TODO @gbotrel check serialization round trip with constraint system. @@ -85,7 +85,7 @@ func (assert *Assert) CheckCircuit(circuit frontend.Circuit, opts ...TestingOpti w := w assert.Run(func(assert *Assert) { _, err = ccs.Solve(w.full, opt.solverOpts...) - assert.error(err, &w) + assert.error(curve.ScalarField(), err, &w) }, "invalid_witness") } @@ -93,7 +93,7 @@ func (assert *Assert) CheckCircuit(circuit frontend.Circuit, opts ...TestingOpti w := w assert.Run(func(assert *Assert) { _, err = ccs.Solve(w.full, opt.solverOpts...) - assert.noError(err, &w) + assert.noError(curve.ScalarField(), err, &w) }, "valid_witness") } @@ -116,7 +116,7 @@ func (assert *Assert) CheckCircuit(circuit frontend.Circuit, opts ...TestingOpti // proof system setup. pk, vk, pkBuilder, vkBuilder, proofBuilder, err := concreteBackend.setup(ccs, curve) - assert.noError(err, nil) + assert.noError(curve.ScalarField(), err, nil) // for each valid witness, run the prover and verifier for _, w := range validWitnesses { @@ -138,10 +138,10 @@ func (assert *Assert) CheckCircuit(circuit frontend.Circuit, opts ...TestingOpti verifierOpts = append([]backend.VerifierOption{solidity.WithVerifierTargetSolidityVerifier(b)}, opt.verifierOpts...) } proof, err := concreteBackend.prove(ccs, pk, w.full, proverOpts...) - assert.noError(err, &w) + assert.noError(curve.ScalarField(), err, &w) err = concreteBackend.verify(proof, vk, w.public, verifierOpts...) - assert.noError(err, &w) + assert.noError(curve.ScalarField(), err, &w) if checkSolidity { // check that the proof can be verified by gnark-solidity-checker @@ -162,7 +162,7 @@ func (assert *Assert) CheckCircuit(circuit frontend.Circuit, opts ...TestingOpti w := w assert.Run(func(assert *Assert) { _, err := concreteBackend.prove(ccs, pk, w.full, opt.proverOpts...) - assert.error(err, &w) + assert.error(curve.ScalarField(), err, &w) }, "invalid_witness") } @@ -217,16 +217,16 @@ func (assert *Assert) parseAssignment(circuit frontend.Circuit, assignment front // count number of element in witness. // if too many, we don't do JSON serialization. - s, err := schema.Walk(assignment, tVariable, nil) + s, err := schema.Walk(curve.ScalarField(), assignment, tVariable, nil) assert.NoError(err) if s.Public+s.Secret <= serializationThreshold { assert.Run(func(assert *Assert) { - s := lazySchema(circuit)() + s := lazySchema(curve.ScalarField(), circuit)() assert.marshalWitnessJSON(full, s, curve, false) }, curve.String(), "marshal/json") assert.Run(func(assert *Assert) { - s := lazySchema(circuit)() + s := lazySchema(curve.ScalarField(), circuit)() assert.marshalWitnessJSON(public, s, curve, true) }, curve.String(), "marshal-public/json") } diff --git a/test/assert_fuzz.go b/test/assert_fuzz.go index 85564491..ed93ae6c 100644 --- a/test/assert_fuzz.go +++ b/test/assert_fuzz.go @@ -69,7 +69,7 @@ func init() { type filler func(frontend.Circuit, ecc.ID) func zeroFiller(w frontend.Circuit, curve ecc.ID) { - fill(w, func() interface{} { + fill(w, curve, func() interface{} { return 0 }) } @@ -77,7 +77,7 @@ func zeroFiller(w frontend.Circuit, curve ecc.ID) { func binaryFiller(w frontend.Circuit, curve ecc.ID) { mrand := mrand.New(mrand.NewSource(time.Now().Unix())) //#nosec G404 weak rng is fine here - fill(w, func() interface{} { + fill(w, curve, func() interface{} { return int(mrand.Uint32() % 2) //#nosec G404 weak rng is fine here }) } @@ -88,7 +88,7 @@ func seedFiller(w frontend.Circuit, curve ecc.ID) { m := curve.ScalarField() - fill(w, func() interface{} { + fill(w, curve, func() interface{} { i := int(mrand.Uint32() % uint32(len(seedCorpus))) //#nosec G404 weak rng is fine here r := new(big.Int).Set(seedCorpus[i]) return r.Mod(r, m) @@ -100,7 +100,7 @@ func randomFiller(w frontend.Circuit, curve ecc.ID) { r := mrand.New(mrand.NewSource(time.Now().Unix())) //#nosec G404 weak rng is fine here m := curve.ScalarField() - fill(w, func() interface{} { + fill(w, curve, func() interface{} { i := int(mrand.Uint32() % uint32(len(seedCorpus)*2)) //#nosec G404 weak rng is fine here if i >= len(seedCorpus) { b1, _ := rand.Int(r, m) //#nosec G404 weak rng is fine here @@ -111,7 +111,7 @@ func randomFiller(w frontend.Circuit, curve ecc.ID) { }) } -func fill(w frontend.Circuit, nextValue func() interface{}) { +func fill(w frontend.Circuit, curve ecc.ID, nextValue func() interface{}) { setHandler := func(f schema.LeafInfo, tInput reflect.Value) error { v := nextValue() tInput.Set(reflect.ValueOf((v))) @@ -119,7 +119,7 @@ func fill(w frontend.Circuit, nextValue func() interface{}) { } // this can't error. // TODO @gbotrel it might error with .Walk? - _, _ = schema.Walk(w, tVariable, setHandler) + _, _ = schema.Walk(curve.ScalarField(), w, tVariable, setHandler) } var tVariable reflect.Type @@ -180,7 +180,7 @@ func (assert *Assert) fuzzer(fuzzer filler, circuit, w frontend.Circuit, b backe if err != nil { panic(err) } - s, err := frontend.NewSchema(circuit) + s, err := frontend.NewSchema(curve.ScalarField(), circuit) if err != nil { panic(err) } @@ -210,7 +210,7 @@ func (assert *Assert) solvingSucceeded(circuit frontend.Circuit, validAssignment // parse assignment w := assert.parseAssignment(circuit, validAssignment, curve, opt.checkSerialization) - checkError := func(err error) { assert.noError(err, &w) } + checkError := func(err error) { assert.noError(curve.ScalarField(), err, &w) } // 1- compile the circuit ccs, err := assert.compile(circuit, curve, b, opt.compileOpts) @@ -229,8 +229,8 @@ func (assert *Assert) solvingFailed(circuit frontend.Circuit, invalidAssignment // parse assignment w := assert.parseAssignment(circuit, invalidAssignment, curve, opt.checkSerialization) - checkError := func(err error) { assert.noError(err, &w) } - mustError := func(err error) { assert.error(err, &w) } + checkError := func(err error) { assert.noError(curve.ScalarField(), err, &w) } + mustError := func(err error) { assert.error(curve.ScalarField(), err, &w) } // 1- compile the circuit ccs, err := assert.compile(circuit, curve, b, opt.compileOpts) diff --git a/test/blueprint_solver.go b/test/blueprint_solver.go index 9ced90ee..fe0b636c 100644 --- a/test/blueprint_solver.go +++ b/test/blueprint_solver.go @@ -4,19 +4,20 @@ import ( "math/big" "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/internal/smallfields" "github.com/consensys/gnark/internal/utils" ) // blueprintSolver is a constraint.Solver that can be used to test a circuit // it is a separate type to avoid method collisions with the engine. -type blueprintSolver struct { +type blueprintSolver[E constraint.Element] struct { internalVariables []*big.Int q *big.Int } // implements constraint.Solver -func (s *blueprintSolver) SetValue(vID uint32, f constraint.Element) { +func (s *blueprintSolver[E]) SetValue(vID uint32, f E) { if int(vID) > len(s.internalVariables) { panic("out of bounds") } @@ -24,117 +25,135 @@ func (s *blueprintSolver) SetValue(vID uint32, f constraint.Element) { s.internalVariables[vID].Set(v) } -func (s *blueprintSolver) GetValue(cID, vID uint32) constraint.Element { +func (s *blueprintSolver[E]) GetValue(cID, vID uint32) E { panic("not implemented in test.Engine") } -func (s *blueprintSolver) GetCoeff(cID uint32) constraint.Element { +func (s *blueprintSolver[E]) GetCoeff(cID uint32) E { panic("not implemented in test.Engine") } -func (s *blueprintSolver) IsSolved(vID uint32) bool { +func (s *blueprintSolver[E]) IsSolved(vID uint32) bool { panic("not implemented in test.Engine") } // implements constraint.Field -func (s *blueprintSolver) FromInterface(i interface{}) constraint.Element { +func (s *blueprintSolver[E]) FromInterface(i interface{}) E { b := utils.FromInterface(i) return s.toElement(&b) } -func (s *blueprintSolver) ToBigInt(f constraint.Element) *big.Int { +func (s *blueprintSolver[E]) ToBigInt(f E) *big.Int { r := new(big.Int) fBytes := f.Bytes() r.SetBytes(fBytes[:]) return r } -func (s *blueprintSolver) Mul(a, b constraint.Element) constraint.Element { +func (s *blueprintSolver[E]) Mul(a, b E) E { ba, bb := s.ToBigInt(a), s.ToBigInt(b) ba.Mul(ba, bb).Mod(ba, s.q) return s.toElement(ba) } -func (s *blueprintSolver) Add(a, b constraint.Element) constraint.Element { +func (s *blueprintSolver[E]) Add(a, b E) E { ba, bb := s.ToBigInt(a), s.ToBigInt(b) ba.Add(ba, bb).Mod(ba, s.q) return s.toElement(ba) } -func (s *blueprintSolver) Sub(a, b constraint.Element) constraint.Element { +func (s *blueprintSolver[E]) Sub(a, b E) E { ba, bb := s.ToBigInt(a), s.ToBigInt(b) ba.Sub(ba, bb).Mod(ba, s.q) return s.toElement(ba) } -func (s *blueprintSolver) Neg(a constraint.Element) constraint.Element { +func (s *blueprintSolver[E]) Neg(a E) E { ba := s.ToBigInt(a) ba.Neg(ba).Mod(ba, s.q) return s.toElement(ba) } -func (s *blueprintSolver) Inverse(a constraint.Element) (constraint.Element, bool) { +func (s *blueprintSolver[E]) Inverse(a E) (E, bool) { ba := s.ToBigInt(a) r := ba.ModInverse(ba, s.q) return s.toElement(ba), r != nil } -func (s *blueprintSolver) One() constraint.Element { +func (s *blueprintSolver[E]) One() E { b := new(big.Int).SetUint64(1) return s.toElement(b) } -func (s *blueprintSolver) IsOne(a constraint.Element) bool { +func (s *blueprintSolver[E]) IsOne(a E) bool { b := s.ToBigInt(a) return b.IsUint64() && b.Uint64() == 1 } -func (s *blueprintSolver) String(a constraint.Element) string { +func (s *blueprintSolver[E]) String(a E) string { b := s.ToBigInt(a) return b.String() } -func (s *blueprintSolver) Uint64(a constraint.Element) (uint64, bool) { +func (s *blueprintSolver[E]) Uint64(a E) (uint64, bool) { b := s.ToBigInt(a) return b.Uint64(), b.IsUint64() } -func (s *blueprintSolver) Read(calldata []uint32) (constraint.Element, int) { +func (s *blueprintSolver[E]) Read(calldata []uint32) (E, int) { // We encoded big.Int as constraint.Element on 12 uint32 words. - var r constraint.Element - for i := 0; i < len(r); i++ { - index := i * 2 - r[i] = uint64(calldata[index])<<32 | uint64(calldata[index+1]) + var r E + switch t := any(&r).(type) { + case *constraint.U64: + for i := 0; i < len(r); i++ { + index := i * 2 + t[i] = uint64(calldata[index])<<32 | uint64(calldata[index+1]) + } + return r, len(r) * 2 + case *constraint.U32: + t[0] = uint32(calldata[0]) + return r, 1 + default: + panic("unsupported type") } - return r, len(r) * 2 } -func (s *blueprintSolver) toElement(b *big.Int) constraint.Element { - return bigIntToElement(b) +func (s *blueprintSolver[E]) toElement(b *big.Int) E { + return bigIntToElement[E](b) } -func bigIntToElement(b *big.Int) constraint.Element { +func bigIntToElement[E constraint.Element](b *big.Int) E { if b.Sign() == -1 { panic("negative value") } bytes := b.Bytes() - if len(bytes) > 48 { + var bytesLen int + var r E + switch any(r).(type) { + case constraint.U32: + bytesLen = 4 + case constraint.U64: + bytesLen = 48 + default: + panic("unsupported type") + } + if len(bytes) > bytesLen { panic("value too big") } - var paddedBytes [48]byte - copy(paddedBytes[48-len(bytes):], bytes[:]) - - var r constraint.Element - r.SetBytes(paddedBytes) - - return r + paddedBytes := make([]byte, bytesLen) + copy(paddedBytes[bytesLen-len(bytes):], bytes[:]) + return constraint.NewElement[E](paddedBytes[:]) } // wrappedBigInt is a wrapper around big.Int to implement the frontend.CanonicalVariable interface type wrappedBigInt struct { *big.Int + modulus *big.Int } func (w wrappedBigInt) Compress(to *[]uint32) { - // convert to Element. - e := bigIntToElement(w.Int) - - // append the uint32 words to the slice - for i := 0; i < len(e); i++ { - *to = append(*to, uint32(e[i]>>32)) - *to = append(*to, uint32(e[i]&0xffffffff)) + if smallfields.IsSmallField(w.modulus) { + e := bigIntToElement[constraint.U32](w.Int) + *to = append(*to, uint32(e[0])) + } else { + e := bigIntToElement[constraint.U64](w.Int) + // append the uint32 words to the slice + for i := 0; i < len(e); i++ { + *to = append(*to, uint32(e[i]>>32)) + *to = append(*to, uint32(e[i]&0xffffffff)) + } } } diff --git a/test/blueprint_solver_test.go b/test/blueprint_solver_test.go index 0cc42b6a..675ab306 100644 --- a/test/blueprint_solver_test.go +++ b/test/blueprint_solver_test.go @@ -7,13 +7,14 @@ import ( "time" "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/field/babybear" + "github.com/consensys/gnark/constraint" ) -func TestBigIntToElement(t *testing.T) { - t.Parallel() +func testBigIntoToElement[E constraint.Element](t *testing.T, modulus *big.Int) { // sample a random big.Int, convert it to an element, and back // to a big.Int, and check that it's the same - s := blueprintSolver{q: ecc.BN254.ScalarField()} + s := blueprintSolver[E]{q: modulus} b := big.NewInt(0) for i := 0; i < 50; i++ { b.Rand(rand.New(rand.NewSource(time.Now().Unix())), s.q) //#nosec G404 -- This is a false positive @@ -23,36 +24,52 @@ func TestBigIntToElement(t *testing.T) { t.Fatal("b != b2") } } +} +func TestBigIntToElement(t *testing.T) { + // t.Parallel() + testBigIntoToElement[constraint.U64](t, ecc.BW6_761.ScalarField()) + testBigIntoToElement[constraint.U64](t, ecc.BN254.ScalarField()) + testBigIntoToElement[constraint.U32](t, babybear.Modulus()) } -func TestBigIntToUint32Slice(t *testing.T) { - t.Parallel() +func testBigIntToUint32Slice[E constraint.Element](t *testing.T, modulus *big.Int) { // sample a random big.Int, write it to a uint32 slice, and back // to a big.Int, and check that it's the same - s := blueprintSolver{q: ecc.BN254.ScalarField()} + s := blueprintSolver[E]{q: modulus} + var elementLen int // number of uint32 words in the element + var e E + switch any(e).(type) { + case constraint.U32: + elementLen = 1 // 2 * 1 uint32 + case constraint.U64: + elementLen = 12 // 6 * 2 ([6]uint64) = 12 uint32 + } + b1 := big.NewInt(0) b2 := big.NewInt(0) + randSource := rand.New(rand.NewSource(time.Now().Unix())) //#nosec G404 -- This is a false positive + for i := 0; i < 50; i++ { - b1.Rand(rand.New(rand.NewSource(time.Now().Unix())), s.q) //#nosec G404 -- This is a false positive - b2.Rand(rand.New(rand.NewSource(time.Now().Unix())), s.q) //#nosec G404 -- This is a false positive - wb1 := wrappedBigInt{b1} - wb2 := wrappedBigInt{b2} + b1.Rand(randSource, s.q) + b2.Rand(randSource, s.q) + wb1 := wrappedBigInt{Int: b1, modulus: modulus} + wb2 := wrappedBigInt{Int: b2, modulus: modulus} var to []uint32 wb1.Compress(&to) wb2.Compress(&to) - if len(to) != 24 { + if len(to) != elementLen*2 { t.Fatal("wrong length: expected 2*len of constraint.Element (uint32 words)") } e1, n := s.Read(to) - if n != 12 { + if n != elementLen { t.Fatal("wrong length: expected 1 len of constraint.Element (uint32 words)") } e2, n := s.Read(to[n:]) - if n != 12 { + if n != elementLen { t.Fatal("wrong length: expected 1 len of constraint.Element (uint32 words)") } rb1, rb2 := s.ToBigInt(e1), s.ToBigInt(e2) @@ -60,5 +77,11 @@ func TestBigIntToUint32Slice(t *testing.T) { t.Fatal("rb1 != b1 || rb2 != b2") } } +} +func TestBigIntToUint32Slice(t *testing.T) { + t.Parallel() + testBigIntToUint32Slice[constraint.U64](t, ecc.BW6_761.ScalarField()) + testBigIntToUint32Slice[constraint.U64](t, ecc.BN254.ScalarField()) + testBigIntToUint32Slice[constraint.U32](t, babybear.Modulus()) } diff --git a/test/engine.go b/test/engine.go index c4c42df5..79322af4 100644 --- a/test/engine.go +++ b/test/engine.go @@ -15,6 +15,7 @@ import ( "github.com/bits-and-blooms/bitset" "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/internal/gkr/gkrinfo" "github.com/consensys/gnark/constraint/solver" "github.com/consensys/gnark/debug" @@ -24,10 +25,10 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark-crypto/field/pool" - "github.com/consensys/gnark/backend" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/internal/circuitdefer" "github.com/consensys/gnark/internal/kvstore" + "github.com/consensys/gnark/internal/smallfields" "github.com/consensys/gnark/internal/utils" ) @@ -40,12 +41,12 @@ import ( type engine struct { curveID ecc.ID q *big.Int - opt backend.ProverConfig // mHintsFunctions map[hint.ID]hintFunction constVars bool kvstore.Store - blueprints []constraint.Blueprint - internalVariables []*big.Int + blueprints []constraint.Blueprint + internalVariables []*big.Int + noSmallFieldCompatibility bool } // TestEngineOption defines an option for the test engine. @@ -62,15 +63,16 @@ func SetAllVariablesAsConstants() TestEngineOption { } } -// WithBackendProverOptions is a test engine option which allows to define -// prover options. If not set, then default prover configuration is used. -func WithBackendProverOptions(opts ...backend.ProverOption) TestEngineOption { +// WithNoSmallFieldCompatibility prevents trying to make the test engine +// compatible with the different backends in case the circuit is compiled over +// a small field. Particularly, in the compatibility mode, the test engine +// would implement [frontend.WideCommitter] and [frontend.Rangechecker] interfaces. +// When this option is set, the test engine will not implement these interfaces. +// +// It is useful for checking edge cases. +func WithNoSmallFieldCompatibility() TestEngineOption { return func(e *engine) error { - cfg, err := backend.NewProverConfig(opts...) - if err != nil { - return fmt.Errorf("new prover config: %w", err) - } - e.opt = cfg + e.noSmallFieldCompatibility = true return nil } } @@ -104,7 +106,7 @@ func IsSolved(circuit, witness frontend.Circuit, field *big.Int, opts ...TestEng c := shallowClone(circuit) // set the witness values - copyWitness(c, witness) + e.copyWitness(c, witness) defer func() { if r := recover(); r != nil { @@ -116,17 +118,29 @@ func IsSolved(circuit, witness frontend.Circuit, field *big.Int, opts ...TestEng log.Debug().Msg("running circuit in test engine") cptAdd, cptMul, cptSub, cptToBinary, cptFromBinary, cptAssertIsEqual = 0, 0, 0, 0, 0, 0 - // first we reset the stateful blueprints - for i := range e.blueprints { - if b, ok := e.blueprints[i].(constraint.BlueprintStateful); ok { - b.Reset() + // XXX(@ivokub): commented out - this seems to match the implementation of native solver, + // but we always create new test engine when calling `IsSolved`, so this slice is always empty. + // Skipping this allows us to avoid making test engine generic. + /* + // first we reset the stateful blueprints + for i := range e.blueprints { + if b, ok := e.blueprints[i].(constraint.BlueprintStateful); ok { + b.Reset() + } } + */ + + var apiEngine frontend.API + if smallfields.IsSmallField(e.modulus()) && !e.noSmallFieldCompatibility { + apiEngine = &smallfieldEngine{engine: e} + } else { + apiEngine = e } - if err = c.Define(e); err != nil { + if err = c.Define(apiEngine); err != nil { return fmt.Errorf("define: %w", err) } - if err = callDeferred(e); err != nil { + if err = callDeferred(apiEngine); err != nil { return fmt.Errorf("deferred: %w", err) } @@ -140,7 +154,7 @@ func IsSolved(circuit, witness frontend.Circuit, field *big.Int, opts ...TestEng return } -func callDeferred(builder *engine) error { +func callDeferred(builder frontend.API) error { for i := 0; i < len(circuitdefer.GetAll[func(frontend.API) error](builder)); i++ { if err := circuitdefer.GetAll[func(frontend.API) error](builder)[i](builder); err != nil { return fmt.Errorf("defer fn %d: %w", i, err) @@ -554,11 +568,6 @@ func (e *engine) NewHintForId(id solver.HintID, nbOutputs int, inputs ...fronten return nil, fmt.Errorf("no hint registered with id #%d. Use solver.RegisterHint or solver.RegisterNamedHint", id) } -// IsConstant returns true if v is a constant known at compile time -func (e *engine) IsConstant(v frontend.Variable) bool { - return e.constVars -} - // ConstantValue returns the big.Int value of v func (e *engine) ConstantValue(v frontend.Variable) (*big.Int, bool) { r := e.toBigInt(v) @@ -625,7 +634,7 @@ func shallowClone(circuit frontend.Circuit) frontend.Circuit { return circuitCopy } -func copyWitness(to, from frontend.Circuit) { +func (e *engine) copyWitness(to, from frontend.Circuit) { var wValues []reflect.Value collectHandler := func(f schema.LeafInfo, tInput reflect.Value) error { @@ -636,18 +645,27 @@ func copyWitness(to, from frontend.Circuit) { wValues = append(wValues, tInput) return nil } - if _, err := schema.Walk(from, tVariable, collectHandler); err != nil { + if _, err := schema.Walk(e.Field(), from, tVariable, collectHandler); err != nil { panic(err) } i := 0 setHandler := func(f schema.LeafInfo, tInput reflect.Value) error { - tInput.Set(wValues[i]) + wValueIntf := wValues[i].Interface() + val := utils.FromInterface(wValueIntf) + if val.Cmp(e.modulus()) >= 0 { + val.Mod(&val, e.modulus()) + } + if val.Sign() < 0 { + val.Add(&val, e.modulus()) + } + wValueReduced := reflect.ValueOf(val) + tInput.Set(wValueReduced) i++ return nil } // this can't error. - _, _ = schema.Walk(to, tVariable, setHandler) + _, _ = schema.Walk(e.Field(), to, tVariable, setHandler) } @@ -660,6 +678,9 @@ func (e *engine) Compiler() frontend.Compiler { } func (e *engine) Commit(v ...frontend.Variable) (frontend.Variable, error) { + if smallfields.IsSmallField(e.modulus()) { + panic("commitment not supported for small fields") + } nb := (e.FieldBitLen() + 7) / 8 buf := make([]byte, nb) hasher := sha3.NewCShake128(nil, []byte("gnark test engine")) @@ -683,10 +704,8 @@ func (e *engine) Defer(cb func(frontend.API) error) { circuitdefer.Put(e, cb) } -// AddInstruction is used to add custom instructions to the constraint system. -// In constraint system, this is asynchronous. In here, we do it synchronously. -func (e *engine) AddInstruction(bID constraint.BlueprintID, calldata []uint32) []uint32 { - blueprint := e.blueprints[bID].(constraint.BlueprintSolvable) +func addInstructionGeneric[E constraint.Element](e *engine, bID constraint.BlueprintID, calldata []uint32) []uint32 { + blueprint := e.blueprints[bID].(constraint.BlueprintSolvable[E]) // create a dummy instruction inst := constraint.Instruction{ @@ -704,7 +723,7 @@ func (e *engine) AddInstruction(bID constraint.BlueprintID, calldata []uint32) [ } // solve the blueprint synchronously - s := blueprintSolver{ + s := blueprintSolver[E]{ internalVariables: e.internalVariables, q: e.q, } @@ -715,10 +734,29 @@ func (e *engine) AddInstruction(bID constraint.BlueprintID, calldata []uint32) [ return r } +// AddInstruction is used to add custom instructions to the constraint system. +// In constraint system, this is asynchronous. In here, we do it synchronously. +func (e *engine) AddInstruction(bID constraint.BlueprintID, calldata []uint32) []uint32 { + if constraint.FitsElement[constraint.U32](e.q) { + return addInstructionGeneric[constraint.U32](e, bID, calldata) + } + if constraint.FitsElement[constraint.U64](e.q) { + return addInstructionGeneric[constraint.U64](e, bID, calldata) + } + panic("unsupported field") +} + // AddBlueprint adds a custom blueprint to the constraint system. func (e *engine) AddBlueprint(b constraint.Blueprint) constraint.BlueprintID { - if _, ok := b.(constraint.BlueprintSolvable); !ok { - panic("unsupported blueprint in test engine") + if constraint.FitsElement[constraint.U32](e.q) { + if _, ok := b.(constraint.BlueprintSolvable[constraint.U32]); !ok { + panic("unsupported blueprint in test engine") + } + } + if constraint.FitsElement[constraint.U64](e.q) { + if _, ok := b.(constraint.BlueprintSolvable[constraint.U64]); !ok { + panic("unsupported blueprint in test engine") + } } e.blueprints = append(e.blueprints, b) return constraint.BlueprintID(len(e.blueprints) - 1) @@ -738,10 +776,10 @@ func (e *engine) InternalVariable(vID uint32) frontend.Variable { // this is used in custom blueprints to return a variable than can be encoded in blueprints func (e *engine) ToCanonicalVariable(v frontend.Variable) frontend.CanonicalVariable { r := e.toBigInt(v) - return wrappedBigInt{r} + return wrappedBigInt{Int: r, modulus: e.q} } -func (e *engine) SetGkrInfo(info constraint.GkrInfo) error { +func (e *engine) SetGkrInfo(gkrinfo.StoringInfo) error { return nil } @@ -768,3 +806,37 @@ func (e *engine) MustBeLessOrEqCst(aBits []frontend.Variable, bound *big.Int, aF panic(fmt.Sprintf("%d > %d", v, bound)) } } + +type smallfieldEngine struct { + *engine +} + +func (e *smallfieldEngine) WideCommit(width int, v ...frontend.Variable) ([]frontend.Variable, error) { + nb := (e.FieldBitLen() + 7) / 8 + buf := make([]byte, nb) + hasher := sha3.NewCShake128(nil, []byte("gnark test engine")) + for i := range v { + vs := e.toBigInt(v[i]) + bs := vs.FillBytes(buf) + hasher.Write(bs) + } + res := make([]frontend.Variable, width) + for i := 0; i < width; i++ { + hasher.Read(buf) + resi := new(big.Int).SetBytes(buf) + resi.Mod(resi, e.modulus()) + res[i] = new(big.Int).Set(resi) + } + return res, nil +} + +func (e *smallfieldEngine) Check(in frontend.Variable, width int) { + bin := e.toBigInt(in) + if bin.BitLen() > width { + panic(fmt.Sprintf("range check failed: %s (bitLen == %d) with %d bits", bin.String(), bin.BitLen(), width)) + } +} + +func (e *smallfieldEngine) Compiler() frontend.Compiler { + return e +} diff --git a/test/solver_test.go b/test/solver_test.go index 88e0d39b..dc27a645 100644 --- a/test/solver_test.go +++ b/test/solver_test.go @@ -19,7 +19,7 @@ import ( "github.com/consensys/gnark/frontend/schema" "github.com/consensys/gnark/internal/backend/circuits" "github.com/consensys/gnark/internal/kvstore" - "github.com/consensys/gnark/internal/tinyfield" + "github.com/consensys/gnark/internal/smallfields/tinyfield" "github.com/consensys/gnark/internal/utils" ) @@ -29,7 +29,7 @@ const permutterBound = 3 // r1cs + sparser1cs const nbSystems = 2 -var builders [2]frontend.NewBuilder +var builders [2]frontend.NewBuilderU32 func TestSolverConsistency(t *testing.T) { if testing.Short() { @@ -45,6 +45,12 @@ func TestSolverConsistency(t *testing.T) { for name := range circuits.Circuits { t.Run(name, func(t *testing.T) { + if name == "commit" { + // we skip the commit circuit for consistency check because small field circuits + // should use [frontend.WideCommitter] interface, but in this test we want to + // use the given builder not, the one wrapped using [widecommitter.From]. + return + } tc := circuits.Circuits[name] t.Parallel() err := consistentSolver(tc.Circuit, tc.HintFunctions) @@ -105,7 +111,7 @@ func newPermutterWitness(pv tinyfield.Vector) witness.Witness { type permutter struct { circuit frontend.Circuit - constraintSystems [2]constraint.ConstraintSystem + constraintSystems [2]constraint.ConstraintSystemU32 witness []tinyfield.Element hints []solver.Hint } @@ -114,7 +120,7 @@ type permutter struct { func (p *permutter) permuteAndTest(index int) error { for i := 0; i < len(tinyfieldElements); i++ { - p.witness[index].SetUint64(tinyfieldElements[i]) + p.witness[index].SetUint64(uint64(tinyfieldElements[i])) if index == len(p.witness)-1 { // we have a unique permutation @@ -128,10 +134,10 @@ func (p *permutter) permuteAndTest(index int) error { // solve the cs using test engine // first copy the witness in the circuit - copyWitnessFromVector(p.circuit, p.witness) + copyWitnessFromVector(tinyfield.Modulus(), p.circuit, p.witness) errorEngines[0] = isSolvedEngine(p.circuit, tinyfield.Modulus()) - copyWitnessFromVector(p.circuit, p.witness) + copyWitnessFromVector(tinyfield.Modulus(), p.circuit, p.witness) errorEngines[1] = isSolvedEngine(p.circuit, tinyfield.Modulus(), SetAllVariablesAsConstants()) } @@ -217,9 +223,9 @@ func isSolvedEngine(c frontend.Circuit, field *big.Int, opts ...TestEngineOption // fill the "to" frontend.Circuit with values from the provided vector // values are assumed to be ordered [public | secret] -func copyWitnessFromVector(to frontend.Circuit, from []tinyfield.Element) { +func copyWitnessFromVector(field *big.Int, to frontend.Circuit, from []tinyfield.Element) { i := 0 - schema.Walk(to, tVariable, func(f schema.LeafInfo, tInput reflect.Value) error { + schema.Walk(field, to, tVariable, func(f schema.LeafInfo, tInput reflect.Value) error { if f.Visibility == schema.Public { tInput.Set(reflect.ValueOf(from[i])) i++ @@ -227,7 +233,7 @@ func copyWitnessFromVector(to frontend.Circuit, from []tinyfield.Element) { return nil }) - schema.Walk(to, tVariable, func(f schema.LeafInfo, tInput reflect.Value) error { + schema.Walk(field, to, tVariable, func(f schema.LeafInfo, tInput reflect.Value) error { if f.Visibility == schema.Secret { tInput.Set(reflect.ValueOf(from[i])) i++ @@ -249,7 +255,7 @@ func consistentSolver(circuit frontend.Circuit, hintFunctions []solver.Hint) err // compile the systems for i := 0; i < nbSystems; i++ { - ccs, err := frontend.Compile(tinyfield.Modulus(), builders[i], circuit) + ccs, err := frontend.CompileU32(tinyfield.Modulus(), builders[i], circuit) if err != nil { return err }