From 700e9da9195aa1f2fb5f87c9c599ee59a8735b93 Mon Sep 17 00:00:00 2001 From: mattstam Date: Wed, 7 Feb 2024 13:18:27 -0800 Subject: [PATCH] support both proving systems --- go.mod | 5 +- go.sum | 17 +- plonky2x/verifier/circuit.go | 150 ----- plonky2x/verifier/cli.go | 132 ++--- plonky2x/verifier/prover.go | 198 ------- plonky2x/verifier/system/circuit.go | 69 +++ plonky2x/verifier/system/groth16.go | 415 +++++++++++++ .../groth16_deserializer.go} | 2 +- plonky2x/verifier/system/interface.go | 8 + plonky2x/verifier/system/plonk.go | 423 ++++++++++++++ plonky2x/verifier/system/proof.go | 70 +++ .../verifier/verifier-build-prod/Verifier.sol | 546 ++++++++++++++++++ plonky2x/verifier/verifier.go | 101 ---- plonky2x/verifier/verifier_test.go | 4 +- 14 files changed, 1577 insertions(+), 563 deletions(-) delete mode 100644 plonky2x/verifier/circuit.go delete mode 100644 plonky2x/verifier/prover.go create mode 100644 plonky2x/verifier/system/circuit.go create mode 100644 plonky2x/verifier/system/groth16.go rename plonky2x/verifier/{vk_deserializer.go => system/groth16_deserializer.go} (99%) create mode 100644 plonky2x/verifier/system/interface.go create mode 100644 plonky2x/verifier/system/plonk.go create mode 100644 plonky2x/verifier/system/proof.go create mode 100644 plonky2x/verifier/verifier-build-prod/Verifier.sol delete mode 100644 plonky2x/verifier/verifier.go diff --git a/go.mod b/go.mod index 80d7dadbf..65f3c56a1 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,11 @@ require ( github.com/consensys/gnark v0.9.1 github.com/consensys/gnark-crypto v0.12.2-0.20231013160410-1f65e75b6dfb github.com/ethereum/go-ethereum v1.12.0 + github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.31.0 github.com/stretchr/testify v1.8.4 github.com/succinctlabs/gnark-plonky2-verifier v0.0.0-20231013210054-89b5a01e4b4b + go.uber.org/zap v1.26.0 ) require ( @@ -31,11 +34,11 @@ require ( github.com/mattn/go-isatty v0.0.19 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rs/zerolog v1.31.0 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/tklauser/go-sysconf v0.3.5 // indirect github.com/tklauser/numcpus v0.2.2 // indirect github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.10.0 // indirect golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/sync v0.3.0 // indirect diff --git a/go.sum b/go.sum index 0c7f38abc..92bfe1d14 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,6 @@ github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 h1:fLjPD/aNc3UIO github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/bits-and-blooms/bitset v1.9.0 h1:g1YivPG8jOtrN013Fe8OBXubkiTwvm7/vG2vXz03ANU= -github.com/bits-and-blooms/bitset v1.9.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -20,15 +18,8 @@ github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 h1:ytcWPaNPhNoG github.com/cockroachdb/redact v1.1.3 h1:AKZds10rFSIj7qADf0g46UixK8NNLwWTNdCIGS5wfSQ= github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= -github.com/consensys/gnark v0.8.0 h1:0bQ2MyDG4oNjMQpNyL8HjrrUSSL3yYJg0Elzo6LzmcU= -github.com/consensys/gnark v0.8.0/go.mod h1:aKmA7dIiLbTm0OV37xTq0z+Bpe4xER8EhRLi6necrm8= -github.com/consensys/gnark v0.9.0 h1:OoOr0Q771mQINVdP3s1AF2Rs1y8gtXhWVkadz/9KmZc= -github.com/consensys/gnark v0.9.0/go.mod h1:Sy9jJjIaGJFfNeupyNOR9Ei2IbAB6cfCO78DfG27YvM= github.com/consensys/gnark v0.9.1 h1:aTwBp5469MY/2jNrf4ABrqHRW3+JytfkADdw4ZBY7T0= github.com/consensys/gnark v0.9.1/go.mod h1:udWvWGXnfBE7mn7BsNoGAvZDnUhcONBEtNijvVjfY80= -github.com/consensys/gnark-crypto v0.11.2/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= -github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= -github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= github.com/consensys/gnark-crypto v0.12.2-0.20231013160410-1f65e75b6dfb h1:f0BMgIjhZy4lSRHCXFbQst85f5agZAjtDMixQqBWNpc= github.com/consensys/gnark-crypto v0.12.2-0.20231013160410-1f65e75b6dfb/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= @@ -103,8 +94,6 @@ github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/succinctlabs/gnark-plonky2-verifier v0.0.0-20231012010246-940c81b212ec h1:xMlCVbabspLQ/sukwyu1WQzznh476x4O2/ee5X8igZ0= -github.com/succinctlabs/gnark-plonky2-verifier v0.0.0-20231012010246-940c81b212ec/go.mod h1:33fqngzJywBvG2tiETIPCFUCnRGkyTOybblVB9M7aOs= github.com/succinctlabs/gnark-plonky2-verifier v0.0.0-20231013210054-89b5a01e4b4b h1:kcqBBMCEhDG6cNKAosD+7OG97jkyih9/G7GuNMrW6A8= github.com/succinctlabs/gnark-plonky2-verifier v0.0.0-20231013210054-89b5a01e4b4b/go.mod h1:33fqngzJywBvG2tiETIPCFUCnRGkyTOybblVB9M7aOs= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= @@ -115,11 +104,15 @@ github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZF github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/plonky2x/verifier/circuit.go b/plonky2x/verifier/circuit.go deleted file mode 100644 index 29c7e001a..000000000 --- a/plonky2x/verifier/circuit.go +++ /dev/null @@ -1,150 +0,0 @@ -package main - -import ( - "fmt" - "math/big" - "os" - "time" - - "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark/backend/groth16" - "github.com/consensys/gnark/constraint" - "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/frontend/cs/r1cs" - "github.com/consensys/gnark/logger" - "github.com/succinctlabs/gnark-plonky2-verifier/types" - "github.com/succinctlabs/gnark-plonky2-verifier/variables" - "github.com/succinctlabs/gnark-plonky2-verifier/verifier" -) - -type Plonky2xVerifierCircuit struct { - // A digest of the plonky2x circuit that is being verified. - VerifierDigest frontend.Variable `gnark:"verifierDigest,public"` - - // The input hash is the hash of all onchain inputs into the function. - InputHash frontend.Variable `gnark:"inputHash,public"` - - // The output hash is the hash of all outputs from the function. - OutputHash frontend.Variable `gnark:"outputHash,public"` - - // Private inputs to the circuit - ProofWithPis variables.ProofWithPublicInputs - VerifierData variables.VerifierOnlyCircuitData - - // Circuit configuration that is not part of the circuit itself. - CommonCircuitData types.CommonCircuitData `gnark:"-"` -} - -func (c *Plonky2xVerifierCircuit) Define(api frontend.API) error { - // initialize the verifier chip - verifierChip := verifier.NewVerifierChip(api, c.CommonCircuitData) - // verify the plonky2 proofD - // _ = verifierChip - verifierChip.Verify(c.ProofWithPis.Proof, c.ProofWithPis.PublicInputs, c.VerifierData) - - // We assume that the publicInputs have 64 bytes - // publicInputs[0:32] is a big-endian representation of a SHA256 hash that has been truncated to 253 bits. - // Note that this truncation happens in the `WrappedCircuit` when computing the `input_hash` - // The reason for truncation is that we only want 1 public input on-chain for the input hash - // to save on gas costs - publicInputs := c.ProofWithPis.PublicInputs - - if len(publicInputs) != 64 { - return fmt.Errorf("expected 64 public inputs, got %d", len(publicInputs)) - } - - inputDigest := frontend.Variable(0) - for i := 0; i < 32; i++ { - pubByte := publicInputs[31-i].Limb - inputDigest = api.Add(inputDigest, api.Mul(pubByte, frontend.Variable(new(big.Int).Lsh(big.NewInt(1), uint(8*i))))) - - } - api.AssertIsEqual(c.InputHash, inputDigest) - - outputDigest := frontend.Variable(0) - for i := 0; i < 32; i++ { - pubByte := publicInputs[63-i].Limb - outputDigest = api.Add(outputDigest, api.Mul(pubByte, frontend.Variable(new(big.Int).Lsh(big.NewInt(1), uint(8*i))))) - } - api.AssertIsEqual(c.OutputHash, outputDigest) - - // We have to assert that the VerifierData we verified the proof with - // matches the VerifierDigest public input. - api.AssertIsEqual(c.VerifierDigest, c.VerifierData.CircuitDigest) - - return nil -} - -func CompileVerifierCircuit(dummyCircuitPath string) (constraint.ConstraintSystem, groth16.ProvingKey, groth16.VerifyingKey, error) { - log := logger.Logger() - verifierOnlyCircuitData := variables.DeserializeVerifierOnlyCircuitData( - types.ReadVerifierOnlyCircuitData(dummyCircuitPath + "/verifier_only_circuit_data.json"), - ) - proofWithPis := variables.DeserializeProofWithPublicInputs( - types.ReadProofWithPublicInputs(dummyCircuitPath + "/proof_with_public_inputs.json"), - ) - commonCircuitData := types.ReadCommonCircuitData(dummyCircuitPath + "/common_circuit_data.json") - - circuit := Plonky2xVerifierCircuit{ - ProofWithPis: proofWithPis, - VerifierData: verifierOnlyCircuitData, - VerifierDigest: new(frontend.Variable), - InputHash: new(frontend.Variable), - OutputHash: new(frontend.Variable), - CommonCircuitData: commonCircuitData, - } - r1cs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &circuit) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to compile circuit: %w", err) - } - - log.Info().Msg("Running circuit setup") - start := time.Now() - pk, vk, err := groth16.Setup(r1cs) - if err != nil { - return nil, nil, nil, err - } - elapsed := time.Since(start) - log.Info().Msg("Successfully ran circuit setup, time: " + elapsed.String()) - - return r1cs, pk, vk, nil -} - -func SaveVerifierCircuit(path string, r1cs constraint.ConstraintSystem, pk groth16.ProvingKey, vk groth16.VerifyingKey) error { - log := logger.Logger() - os.MkdirAll(path, 0755) - log.Info().Msg("Saving circuit constraints to " + path + "/r1cs.bin") - r1csFile, err := os.Create(path + "/r1cs.bin") - if err != nil { - return fmt.Errorf("failed to create r1cs file: %w", err) - } - start := time.Now() - r1cs.WriteTo(r1csFile) - r1csFile.Close() - elapsed := time.Since(start) - log.Debug().Msg("Successfully saved circuit constraints, time: " + elapsed.String()) - - log.Info().Msg("Saving proving key to " + path + "/pk.bin") - pkFile, err := os.Create(path + "/pk.bin") - if err != nil { - return fmt.Errorf("failed to create pk file: %w", err) - } - start = time.Now() - pk.WriteRawTo(pkFile) - pkFile.Close() - elapsed = time.Since(start) - log.Debug().Msg("Successfully saved proving key, time: " + elapsed.String()) - - log.Info().Msg("Saving verifying key to " + path + "/vk.bin") - vkFile, err := os.Create(path + "/vk.bin") - if err != nil { - return fmt.Errorf("failed to create vk file: %w", err) - } - start = time.Now() - vk.WriteRawTo(vkFile) - vkFile.Close() - elapsed = time.Since(start) - log.Info().Msg("Successfully saved verifying key, time: " + elapsed.String()) - - return nil -} diff --git a/plonky2x/verifier/cli.go b/plonky2x/verifier/cli.go index 3d211ff16..2ad5f9fc9 100644 --- a/plonky2x/verifier/cli.go +++ b/plonky2x/verifier/cli.go @@ -1,15 +1,12 @@ package main import ( - "bufio" _ "embed" "flag" - "fmt" "os" - "strings" - "github.com/consensys/gnark/backend/groth16" - "github.com/consensys/gnark/logger" + "github.com/succinctlabs/succinctx/plonky2x/verifier/system" + "go.uber.org/zap" ) func main() { @@ -19,128 +16,67 @@ func main() { dataPath := flag.String("data", "", "data directory") proofFlag := flag.Bool("prove", false, "create a proof") verifyFlag := flag.Bool("verify", false, "verify a proof") - compileFlag := flag.Bool("compile", false, "Compile and save the universal verifier circuit") - contractFlag := flag.Bool("contract", true, "Generate solidity contract") + compileFlag := flag.Bool("compile", false, "compile and save the universal verifier circuit") + exportFlag := flag.Bool("export", false, "export the Solidity verifier") systemFlag := flag.String("system", "groth16", "proving system to use (groth16, plonk)") flag.Parse() - _ = systemFlag - - log := logger.Logger() + logger, err := zap.NewDevelopment() + if err != nil { + panic(err) + } + defer logger.Sync() if *circuitPath == "" { - log.Info().Msg("no circuitPath flag found, so user must input circuitPath via stdin") + logger.Info("no circuitPath flag found, so user must input circuitPath via stdin") } if *dataPath == "" { - log.Error().Msg("please specify a path to data dir (where the compiled gnark circuit data will be)") + logger.Error("please specify a path to data dir (where the compiled gnark circuit data will be)") + os.Exit(1) + } + logger.Info("Circuit path: " + *circuitPath) + logger.Info("Data path: " + *dataPath) + + var s system.ProvingSystem + if *systemFlag == "groth16" { + s = system.NewGroth16System(logger, "./data/dummy", *dataPath) + } else if *systemFlag == "plonk" { + s = system.NewPlonkSystem(logger, "./data/dummy", *dataPath) + } else { + logger.Error("invalid proving system") os.Exit(1) } - - // var system ProvingSystem - // if *systemFlag != "groth16" { - // system = NewGroth16System() - // } - - log.Debug().Msg("Circuit path: " + *circuitPath) - log.Debug().Msg("Data path: " + *dataPath) if *compileFlag { - log.Info().Msg("compiling verifier circuit") - // r1cs, _, _, err := CompileVerifierCircuit("./data/dummy") - // if err != nil { - // log.Error().Msg("failed to compile verifier circuit:" + err.Error()) - // os.Exit(1) - // } - - // pk, vk := getExistingKeys(*dataPath) - - // err = SaveVerifierCircuit(*dataPath, r1cs, pk, vk) - // if err != nil { - // log.Error().Msg("failed to save verifier circuit:" + err.Error()) - // os.Exit(1) - // } - - vk, err := LoadVerifierKey(*dataPath) + err := s.Compile() if err != nil { - panic(err) - } - - if *contractFlag { - log.Info().Msg("generating solidity contract") - err := ExportIFunctionVerifierSolidity(*dataPath, vk) - if err != nil { - log.Error().Msg("failed to generate solidity contract:" + err.Error()) - os.Exit(1) - } + logger.Error("failed to compile verifier circuit:" + err.Error()) + os.Exit(1) } } if *proofFlag { - log.Info().Msg("loading the plonk proving key, circuit data and verifying key") - r1cs, pk, err := LoadProverData(*dataPath) - if err != nil { - log.Err(err).Msg("failed to load the verifier circuit") - os.Exit(1) - } - vk, err := LoadVerifierKey(*dataPath) - if err != nil { - log.Err(err).Msg("failed to load the verifier key") - os.Exit(1) - } - - // If the circuitPath is "" and not provided as part of the CLI flags, then we wait - // for user input. - if *circuitPath == "" { - log.Info().Msg("Waiting for user to provide circuitPath from stdin") - reader := bufio.NewReader(os.Stdin) - str, err := reader.ReadString('\n') - if err != nil { - log.Err(err).Msg("failed to parse the user provided circuitPath") - } - trimmed := strings.TrimSuffix(str, "\n") - circuitPath = &trimmed - } - - log.Info().Msg(fmt.Sprintf("Generating the proof with circuitPath %s", *circuitPath)) - proof, publicWitness, err := Prove(*circuitPath, r1cs, pk) - if err != nil { - log.Err(err).Msg("failed to create the proof") - os.Exit(1) - } - - log.Info().Msg("Verifying proof") - err = groth16.Verify(proof, vk, publicWitness) + err := s.Prove() if err != nil { - log.Err(err).Msg("failed to verify proof") + logger.Error("failed to create proof:" + err.Error()) os.Exit(1) } - log.Info().Msg("Successfully verified proof") } if *verifyFlag { - log.Info().Msg("loading the proof, verifying key and public inputs") - vk, err := LoadVerifierKey(*dataPath) - if err != nil { - log.Err(err).Msg("failed to load the verifier key") - os.Exit(1) - } - publicWitness, err := LoadPublicWitness(*circuitPath) + err := s.Verify() if err != nil { - log.Err(err).Msg("failed to load the public witness") + logger.Error("failed to verify proof:" + err.Error()) os.Exit(1) } + } - proof, err := LoadProof() - if err != nil { - log.Err(err).Msg("failed to load the proof") - os.Exit(1) - } - err = groth16.Verify(proof, vk, publicWitness) + if *exportFlag { + err := s.Export() if err != nil { - log.Err(err).Msg("failed to verify proof") + logger.Error("failed to export verifier circuit:" + err.Error()) os.Exit(1) } - log.Info().Msg("Successfully verified proof") } } diff --git a/plonky2x/verifier/prover.go b/plonky2x/verifier/prover.go deleted file mode 100644 index ee03f308b..000000000 --- a/plonky2x/verifier/prover.go +++ /dev/null @@ -1,198 +0,0 @@ -package main - -import ( - "bufio" - "encoding/json" - "fmt" - "math/big" - "os" - "time" - - "github.com/consensys/gnark-crypto/ecc" - groth16_bn254 "github.com/consensys/gnark/backend/groth16/bn254" - "github.com/ethereum/go-ethereum/common/hexutil" - - "github.com/consensys/gnark/backend/groth16" - "github.com/consensys/gnark/backend/witness" - "github.com/consensys/gnark/constraint" - "github.com/consensys/gnark/frontend" - "github.com/consensys/gnark/logger" - gnark_verifier_types "github.com/succinctlabs/gnark-plonky2-verifier/types" - "github.com/succinctlabs/gnark-plonky2-verifier/variables" - - "github.com/succinctlabs/succinctx/gnarkx/types" -) - -func LoadProvingKey(filepath string) (pk groth16.ProvingKey, err error) { - pk = groth16.NewProvingKey(ecc.BN254) - f, _ := os.Open(filepath + "/pk.bin") - _, err = pk.ReadFrom(f) - if err != nil { - return pk, fmt.Errorf("read file error") - } - f.Close() - - return pk, nil -} - -func LoadProverData(path string) (constraint.ConstraintSystem, groth16.ProvingKey, error) { - log := logger.Logger() - r1csFile, err := os.Open(path + "/r1cs.bin") - if err != nil { - return nil, nil, fmt.Errorf("failed to open r1cs file: %w", err) - } - r1cs := groth16.NewCS(ecc.BN254) - start := time.Now() - r1csReader := bufio.NewReader(r1csFile) - _, err = r1cs.ReadFrom(r1csReader) - if err != nil { - return nil, nil, fmt.Errorf("failed to read r1cs file: %w", err) - } - r1csFile.Close() - elapsed := time.Since(start) - log.Debug().Msg("Successfully loaded constraint system, time: " + elapsed.String()) - - pkFile, err := os.Open(path + "/pk.bin") - if err != nil { - return nil, nil, fmt.Errorf("failed to open pk file: %w", err) - } - pk := groth16.NewProvingKey(ecc.BN254) - start = time.Now() - _, err = pk.ReadFrom(pkFile) - if err != nil { - return nil, nil, fmt.Errorf("failed to read pk file: %w", err) - } - - pkFile.Close() - elapsed = time.Since(start) - log.Debug().Msg("Successfully loaded proving key, time: " + elapsed.String()) - - return r1cs, pk, nil -} - -func GetInputHashOutputHash(proofWithPis gnark_verifier_types.ProofWithPublicInputsRaw) (*big.Int, *big.Int) { - publicInputs := proofWithPis.PublicInputs - if len(publicInputs) != 64 { - panic("publicInputs must be 64 bytes") - } - publicInputsBytes := make([]byte, 64) - for i, v := range publicInputs { - publicInputsBytes[i] = byte(v & 0xFF) - } - inputHash := new(big.Int).SetBytes(publicInputsBytes[0:32]) - outputHash := new(big.Int).SetBytes(publicInputsBytes[32:64]) - if inputHash.BitLen() > 253 { - panic("inputHash must be at most 253 bits") - } - if outputHash.BitLen() > 253 { - panic("outputHash must be at most 253 bits") - } - return inputHash, outputHash -} - -func Prove(circuitPath string, r1cs constraint.ConstraintSystem, pk groth16.ProvingKey) (groth16.Proof, witness.Witness, error) { - log := logger.Logger() - - verifierOnlyCircuitData := variables.DeserializeVerifierOnlyCircuitData( - gnark_verifier_types.ReadVerifierOnlyCircuitData(circuitPath + "/verifier_only_circuit_data.json"), - ) - proofWithPis := gnark_verifier_types.ReadProofWithPublicInputs(circuitPath + "/proof_with_public_inputs.json") - proofWithPisVariable := variables.DeserializeProofWithPublicInputs(proofWithPis) - - inputHash, outputHash := GetInputHashOutputHash(proofWithPis) - - // Circuit assignment - assignment := &Plonky2xVerifierCircuit{ - ProofWithPis: proofWithPisVariable, - VerifierData: verifierOnlyCircuitData, - VerifierDigest: verifierOnlyCircuitData.CircuitDigest, - InputHash: frontend.Variable(inputHash), - OutputHash: frontend.Variable(outputHash), - } - - log.Debug().Msg("Generating witness") - start := time.Now() - witness, err := frontend.NewWitness(assignment, ecc.BN254.ScalarField()) - if err != nil { - return nil, nil, fmt.Errorf("failed to generate witness: %w", err) - } - elapsed := time.Since(start) - log.Debug().Msg("Successfully generated witness, time: " + elapsed.String()) - - log.Debug().Msg("Creating proof") - start = time.Now() - proof, err := groth16.Prove(r1cs, pk, witness) - if err != nil { - return nil, nil, fmt.Errorf("failed to create proof: %w", err) - } - elapsed = time.Since(start) - log.Info().Msg("Successfully created proof, time: " + elapsed.String()) - - _proof := proof.(*groth16_bn254.Proof) - log.Info().Msg("Saving proof to proof.json") - jsonProof, err := json.Marshal(types.ProofResult{ - // Output will be filled in by plonky2x CLI - Output: []byte{}, - Proof: _proof.Ar.Marshal(), - }) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal proof: %w", err) - } - proofFile, err := os.Create("proof.json") - if err != nil { - return nil, nil, fmt.Errorf("failed to create proof file: %w", err) - } - _, err = proofFile.Write(jsonProof) - if err != nil { - return nil, nil, fmt.Errorf("failed to write proof file: %w", err) - } - proofFile.Close() - log.Info().Msg("Successfully saved proof") - - // Write proof with all the public inputs and save to disk. - jsonProofWithWitness, err := json.Marshal(struct { - InputHash hexutil.Bytes `json:"input_hash"` - OutputHash hexutil.Bytes `json:"output_hash"` - VerifierDigest hexutil.Bytes `json:"verifier_digest"` - Proof hexutil.Bytes `json:"proof"` - }{ - InputHash: inputHash.Bytes(), - OutputHash: outputHash.Bytes(), - VerifierDigest: (verifierOnlyCircuitData.CircuitDigest).(*big.Int).Bytes(), - Proof: _proof.Ar.Marshal(), - }) - if err != nil { - return nil, nil, fmt.Errorf("failed to marshal proof with witness: %w", err) - } - proofFile, err = os.Create("proof_with_witness.json") - if err != nil { - return nil, nil, fmt.Errorf("failed to create proof_with_witness file: %w", err) - } - _, err = proofFile.Write(jsonProofWithWitness) - if err != nil { - return nil, nil, fmt.Errorf("failed to write proof_with_witness file: %w", err) - } - proofFile.Close() - log.Info().Msg("Proof with witness") - log.Info().Msg(string(jsonProofWithWitness)) - log.Info().Msg("Successfully saved proof_with_witness") - - publicWitness, err := witness.Public() - if err != nil { - return nil, nil, fmt.Errorf("failed to get public witness: %w", err) - } - - log.Info().Msg("Saving public witness to public_witness.bin") - witnessFile, err := os.Create("public_witness.bin") - if err != nil { - return nil, nil, fmt.Errorf("failed to create public witness file: %w", err) - } - _, err = publicWitness.WriteTo(witnessFile) - if err != nil { - return nil, nil, fmt.Errorf("failed to write public witness file: %w", err) - } - witnessFile.Close() - log.Info().Msg("Successfully saved public witness") - - return proof, publicWitness, nil -} diff --git a/plonky2x/verifier/system/circuit.go b/plonky2x/verifier/system/circuit.go new file mode 100644 index 000000000..5b1bbee77 --- /dev/null +++ b/plonky2x/verifier/system/circuit.go @@ -0,0 +1,69 @@ +package system + +import ( + "fmt" + "math/big" + + "github.com/consensys/gnark/frontend" + "github.com/succinctlabs/gnark-plonky2-verifier/types" + "github.com/succinctlabs/gnark-plonky2-verifier/variables" + "github.com/succinctlabs/gnark-plonky2-verifier/verifier" +) + +type VerifierCircuit struct { + // A digest of the plonky2x circuit that is being verified. + VerifierDigest frontend.Variable `gnark:"verifierDigest,public"` + + // The input hash is the hash of all onchain inputs into the function. + InputHash frontend.Variable `gnark:"inputHash,public"` + + // The output hash is the hash of all outputs from the function. + OutputHash frontend.Variable `gnark:"outputHash,public"` + + // Private inputs to the circuit + ProofWithPis variables.ProofWithPublicInputs + VerifierData variables.VerifierOnlyCircuitData + + // Circuit configuration that is not part of the circuit itself. + CommonCircuitData types.CommonCircuitData `gnark:"-"` +} + +func (c *VerifierCircuit) Define(api frontend.API) error { + // initialize the verifier chip + verifierChip := verifier.NewVerifierChip(api, c.CommonCircuitData) + // verify the plonky2 proofD + // _ = verifierChip + verifierChip.Verify(c.ProofWithPis.Proof, c.ProofWithPis.PublicInputs, c.VerifierData) + + // We assume that the publicInputs have 64 bytes + // publicInputs[0:32] is a big-endian representation of a SHA256 hash that has been truncated to 253 bits. + // Note that this truncation happens in the `WrappedCircuit` when computing the `input_hash` + // The reason for truncation is that we only want 1 public input on-chain for the input hash + // to save on gas costs + publicInputs := c.ProofWithPis.PublicInputs + + if len(publicInputs) != 64 { + return fmt.Errorf("expected 64 public inputs, got %d", len(publicInputs)) + } + + inputDigest := frontend.Variable(0) + for i := 0; i < 32; i++ { + pubByte := publicInputs[31-i].Limb + inputDigest = api.Add(inputDigest, api.Mul(pubByte, frontend.Variable(new(big.Int).Lsh(big.NewInt(1), uint(8*i))))) + + } + api.AssertIsEqual(c.InputHash, inputDigest) + + outputDigest := frontend.Variable(0) + for i := 0; i < 32; i++ { + pubByte := publicInputs[63-i].Limb + outputDigest = api.Add(outputDigest, api.Mul(pubByte, frontend.Variable(new(big.Int).Lsh(big.NewInt(1), uint(8*i))))) + } + api.AssertIsEqual(c.OutputHash, outputDigest) + + // We have to assert that the VerifierData we verified the proof with + // matches the VerifierDigest public input. + api.AssertIsEqual(c.VerifierDigest, c.VerifierData.CircuitDigest) + + return nil +} diff --git a/plonky2x/verifier/system/groth16.go b/plonky2x/verifier/system/groth16.go new file mode 100644 index 000000000..82ec636d2 --- /dev/null +++ b/plonky2x/verifier/system/groth16.go @@ -0,0 +1,415 @@ +package system + +import ( + "bufio" + "bytes" + "encoding/json" + "io" + "math/big" + "os" + "strings" + "time" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/backend/groth16" + groth16Bn254 "github.com/consensys/gnark/backend/groth16/bn254" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/pkg/errors" + "go.uber.org/zap" + + gnark_verifier_types "github.com/succinctlabs/gnark-plonky2-verifier/types" + "github.com/succinctlabs/gnark-plonky2-verifier/variables" +) + +type Groth16System struct { + logger *zap.Logger + circuitPath string + dataPath string +} + +func NewGroth16System(logger *zap.Logger, circuitPath string, dataPath string) *Groth16System { + return &Groth16System{ + logger: logger, + circuitPath: circuitPath, + dataPath: dataPath, + } +} + +func (s *Groth16System) Compile() error { + s.logger.Info("starting compiling verifier circuit") + + r1cs, pk, vk, err := s.CompileVerifierCircuit() + if err != nil { + return errors.Wrap(err, "compile verifier circuit") + } + + err = s.SaveVerifierCircuit(r1cs, pk, vk) + if err != nil { + return errors.Wrap(err, "save verifier circuit") + } + + s.logger.Info("successfully compiled verifier circuit") + + return nil +} + +func (s *Groth16System) Prove() error { + s.logger.Info("starting prove -- loading verifier circuit and proving key") + + // If the circuitPath is "" and not provided as part of the CLI flags, then we wait + // for user input. + if s.circuitPath == "" { + s.logger.Info("no circuitPath flag found, so user must input circuitPath via stdin") + reader := bufio.NewReader(os.Stdin) + str, err := reader.ReadString('\n') + if err != nil { + s.logger.Error("failed to parse the user provided circuitPath", zap.Error(err)) + } + trimmed := strings.TrimSuffix(str, "\n") + s.circuitPath = trimmed + } + + r1cs, err := s.LoadCircuit() + if err != nil { + return errors.Wrap(err, "load the verifier circuit") + } + pk, err := s.LoadProvingKey() + if err != nil { + return errors.Wrap(err, "load the proving key") + } + + _, _, err = s.ProveCircuit(r1cs, pk) + if err != nil { + return errors.Wrap(err, "create proof") + } + + s.logger.Info("successfully created proof") + + return nil +} + +func (s *Groth16System) Verify() error { + s.logger.Info("starting verify -- loading verifier key, public witness, and proof") + + vk, err := s.LoadVerifierKey() + if err != nil { + return errors.Wrap(err, "load verifier key") + } + + proof, err := s.LoadProof() + if err != nil { + return errors.Wrap(err, "load proof") + } + + publicWitness, err := s.LoadPublicWitness() + if err != nil { + return errors.Wrap(err, "load public witness") + } + + err = groth16.Verify(proof, vk, publicWitness) + if err != nil { + return errors.Wrap(err, "verify proof") + } + + s.logger.Info("successfully verified proof") + + return nil +} + +func (s *Groth16System) Export() error { + s.logger.Info("starting export -- loading verifier key and exporting Verifier solidity") + + vk, err := s.LoadVerifierKey() + if err != nil { + return errors.Wrap(err, "load verifier key") + } + + err = s.ExportVerifierSolidity(vk) + if err != nil { + return errors.Wrap(err, "export Verifier solidity") + } + + s.logger.Info("successfully exported Verifier solidity") + + return nil +} + +func (s *Groth16System) CompileVerifierCircuit() (constraint.ConstraintSystem, groth16.ProvingKey, groth16.VerifyingKey, error) { + verifierOnlyCircuitData := variables.DeserializeVerifierOnlyCircuitData( + gnark_verifier_types.ReadVerifierOnlyCircuitData(s.circuitPath + "/verifier_only_circuit_data.json"), + ) + proofWithPis := variables.DeserializeProofWithPublicInputs( + gnark_verifier_types.ReadProofWithPublicInputs(s.circuitPath + "/proof_with_public_inputs.json"), + ) + commonCircuitData := gnark_verifier_types.ReadCommonCircuitData(s.circuitPath + "/common_circuit_data.json") + + circuit := VerifierCircuit{ + ProofWithPis: proofWithPis, + VerifierData: verifierOnlyCircuitData, + VerifierDigest: new(frontend.Variable), + InputHash: new(frontend.Variable), + OutputHash: new(frontend.Variable), + CommonCircuitData: commonCircuitData, + } + r1cs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &circuit) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "compile verifier circuit") + } + + s.logger.Info("Running circuit setup") + start := time.Now() + pk, vk, err := groth16.Setup(r1cs) + if err != nil { + return nil, nil, nil, err + } + elapsed := time.Since(start) + s.logger.Info("Successfully ran circuit setup", zap.String("time", elapsed.String())) + + return r1cs, pk, vk, nil +} + +func (s *Groth16System) SaveVerifierCircuit(r1cs constraint.ConstraintSystem, pk groth16.ProvingKey, vk groth16.VerifyingKey) error { + os.MkdirAll(s.dataPath, 0755) + + r1csFile, err := os.Create(s.dataPath + "/r1cs.bin") + if err != nil { + return errors.Wrap(err, "create r1cs file") + } + r1cs.WriteTo(r1csFile) + r1csFile.Close() + s.logger.Debug("Successfully saved circuit constraints", zap.String("path", s.dataPath+"/r1cs.bin")) + + s.logger.Info("Saving proving key", zap.String("path", s.dataPath+"/pk.bin")) + pkFile, err := os.Create(s.dataPath + "/pk.bin") + if err != nil { + return errors.Wrap(err, "create pk file") + } + pk.WriteRawTo(pkFile) + pkFile.Close() + s.logger.Debug("Successfully saved proving key", zap.String("path", s.dataPath+"/pk.bin")) + + vkFile, err := os.Create(s.dataPath + "/vk.bin") + if err != nil { + return errors.Wrap(err, "create vk file") + } + vk.WriteRawTo(vkFile) + vkFile.Close() + s.logger.Debug("Successfully saved verifying key", zap.String("path", s.dataPath+"/vk.bin")) + + return nil +} + +func (s *Groth16System) ProveCircuit(r1cs constraint.ConstraintSystem, pk groth16.ProvingKey) (groth16.Proof, witness.Witness, error) { + verifierOnlyCircuitData := variables.DeserializeVerifierOnlyCircuitData( + gnark_verifier_types.ReadVerifierOnlyCircuitData(s.circuitPath + "/verifier_only_circuit_data.json"), + ) + proofWithPis := gnark_verifier_types.ReadProofWithPublicInputs(s.circuitPath + "/proof_with_public_inputs.json") + proofWithPisVariable := variables.DeserializeProofWithPublicInputs(proofWithPis) + + inputHash, outputHash := GetInputHashOutputHash(proofWithPis) + + // Circuit assignment + assignment := &VerifierCircuit{ + ProofWithPis: proofWithPisVariable, + VerifierData: verifierOnlyCircuitData, + VerifierDigest: verifierOnlyCircuitData.CircuitDigest, + InputHash: frontend.Variable(inputHash), + OutputHash: frontend.Variable(outputHash), + } + + s.logger.Debug("Generating witness") + start := time.Now() + witness, err := frontend.NewWitness(assignment, ecc.BN254.ScalarField()) + if err != nil { + return nil, nil, errors.Wrap(err, "generate witness") + } + elapsed := time.Since(start) + s.logger.Debug("Successfully generated witness", zap.Duration("time", elapsed)) + + s.logger.Debug("Creating proof") + start = time.Now() + proof, err := groth16.Prove(r1cs, pk, witness) + if err != nil { + return nil, nil, errors.Wrap(err, "create proof") + } + elapsed = time.Since(start) + s.logger.Info("Successfully created proof", zap.Duration("time", elapsed)) + + _proof := proof.(*groth16Bn254.Proof) + s.logger.Info("Saving proof to proof.json") + jsonProof, err := json.Marshal(ProofResult{ + Output: []byte{}, + Proof: _proof.Ar.Marshal(), + }) + if err != nil { + return nil, nil, errors.Wrap(err, "marshal proof") + } + proofFile, err := os.Create("proof.json") + if err != nil { + return nil, nil, errors.Wrap(err, "create proof file") + } + defer proofFile.Close() + if _, err = proofFile.Write(jsonProof); err != nil { + return nil, nil, errors.Wrap(err, "write proof file") + } + s.logger.Info("Successfully saved proof") + + // Write proof with all the public inputs and save to disk. + jsonProofWithWitness, err := json.Marshal(struct { + InputHash hexutil.Bytes `json:"input_hash"` + OutputHash hexutil.Bytes `json:"output_hash"` + VerifierDigest hexutil.Bytes `json:"verifier_digest"` + Proof hexutil.Bytes `json:"proof"` + }{ + InputHash: inputHash.Bytes(), + OutputHash: outputHash.Bytes(), + VerifierDigest: (verifierOnlyCircuitData.CircuitDigest).(*big.Int).Bytes(), + Proof: _proof.Ar.Marshal(), + }) + if err != nil { + return nil, nil, errors.Wrap(err, "marshal proof with witness") + } + proofFile, err = os.Create("proof_with_witness.json") + if err != nil { + return nil, nil, errors.Wrap(err, "create proof_with_witness file") + } + defer proofFile.Close() + if _, err = proofFile.Write(jsonProofWithWitness); err != nil { + return nil, nil, errors.Wrap(err, "write proof_with_witness file") + } + s.logger.Info("Successfully saved proof_with_witness", zap.String("proofWithWitness", string(jsonProofWithWitness))) + + publicWitness, err := witness.Public() + if err != nil { + return nil, nil, errors.Wrap(err, "get public witness") + } + + s.logger.Info("Saving public witness to public_witness.bin") + witnessFile, err := os.Create("public_witness.bin") + if err != nil { + return nil, nil, errors.Wrap(err, "create public witness file") + } + defer witnessFile.Close() + if _, err = publicWitness.WriteTo(witnessFile); err != nil { + return nil, nil, errors.Wrap(err, "write public witness file") + } + s.logger.Info("Successfully saved public witness") + + return proof, publicWitness, nil +} + +func (s *Groth16System) ExportVerifierSolidity(vk groth16.VerifyingKey) error { + // Create a new buffer and export the VerifyingKey into it as a Solidity contract and + // convert the buffer content to a string for further manipulation. + buf := new(bytes.Buffer) + err := vk.ExportSolidity(buf) + if err != nil { + return errors.Wrap(err, "export verifying key to solidity") + } + content := buf.String() + + contractFile, err := os.Create(s.dataPath + "/Verifier.sol") + if err != nil { + return errors.Wrap(err, "create Verifier.sol file") + } + defer contractFile.Close() + + w := bufio.NewWriter(contractFile) + // write the new content to the writer + if _, err = w.Write([]byte(content)); err != nil { + return errors.Wrap(err, "write to Verifier.sol") + } + + if err = w.Flush(); err != nil { + return errors.Wrap(err, "flush writer for Verifier.sol") + } + + return nil +} + +func (s *Groth16System) LoadCircuit() (constraint.ConstraintSystem, error) { + r1cs := groth16.NewCS(ecc.BN254) + f, err := os.Open(s.dataPath + "/r1cs.bin") + if err != nil { + return nil, errors.Wrap(err, "open r1cs file") + } + r1csReader := bufio.NewReader(f) + _, err = r1cs.ReadFrom(r1csReader) + if err != nil { + return nil, errors.Wrap(err, "read r1cs file") + } + f.Close() + + return r1cs, nil +} + +func (s *Groth16System) LoadProvingKey() (pk groth16.ProvingKey, err error) { + pk = groth16.NewProvingKey(ecc.BN254) + f, err := os.Open(s.dataPath + "/pk.bin") + if err != nil { + return pk, errors.Wrap(err, "open pk file") + } + _, err = pk.ReadFrom(f) + if err != nil { + return pk, errors.Wrap(err, "read pk file") + } + f.Close() + + return pk, nil +} + +func (s *Groth16System) LoadVerifierKey() (vk groth16.VerifyingKey, err error) { + vk = groth16.NewVerifyingKey(ecc.BN254) + f, err := os.Open(s.dataPath + "/vk.bin") + if err != nil { + return nil, errors.Wrap(err, "open vk file") + } + _, err = readV08VerifyingKey(vk.(*groth16Bn254.VerifyingKey), f) + if err != nil { + return nil, errors.Wrap(err, "read vk file") + } + f.Close() + + return vk, nil +} + +func (s *Groth16System) LoadProof() (proof groth16.Proof, err error) { + proof = groth16.NewProof(ecc.BN254) + f, err := os.Open(s.dataPath + "/proof.json") + if err != nil { + return proof, errors.Wrap(err, "open proof file") + } + jsonProof, err := io.ReadAll(f) + if err != nil { + return proof, errors.Wrap(err, "read proof file") + } + err = json.Unmarshal(jsonProof, proof) + if err != nil { + return proof, errors.Wrap(err, "read proof file") + } + f.Close() + + return proof, nil +} + +func (s *Groth16System) LoadPublicWitness() (witness.Witness, error) { + publicWitness, err := witness.New(ecc.BN254.ScalarField()) + if err != nil { + return publicWitness, errors.Wrap(err, "create public witness") + } + f, err := os.Open(s.dataPath + "/public_witness.bin") + if err != nil { + return publicWitness, errors.Wrap(err, "open public witness file") + } + _, err = publicWitness.ReadFrom(f) + if err != nil { + return publicWitness, errors.Wrap(err, "read public witness file") + } + f.Close() + + return publicWitness, nil +} diff --git a/plonky2x/verifier/vk_deserializer.go b/plonky2x/verifier/system/groth16_deserializer.go similarity index 99% rename from plonky2x/verifier/vk_deserializer.go rename to plonky2x/verifier/system/groth16_deserializer.go index 2fe12899c..bd540f4f5 100644 --- a/plonky2x/verifier/vk_deserializer.go +++ b/plonky2x/verifier/system/groth16_deserializer.go @@ -1,4 +1,4 @@ -package main +package system import ( "encoding/json" diff --git a/plonky2x/verifier/system/interface.go b/plonky2x/verifier/system/interface.go new file mode 100644 index 000000000..13c1405d1 --- /dev/null +++ b/plonky2x/verifier/system/interface.go @@ -0,0 +1,8 @@ +package system + +type ProvingSystem interface { + Compile() error + Prove() error + Verify() error + Export() error +} diff --git a/plonky2x/verifier/system/plonk.go b/plonky2x/verifier/system/plonk.go new file mode 100644 index 000000000..36ef50007 --- /dev/null +++ b/plonky2x/verifier/system/plonk.go @@ -0,0 +1,423 @@ +package system + +import ( + "bufio" + "bytes" + "encoding/json" + "io" + "math/big" + "os" + "strings" + "time" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark/backend/plonk" + "github.com/consensys/gnark/backend/witness" + "github.com/consensys/gnark/constraint" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/test" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/pkg/errors" + "github.com/rs/zerolog/log" + "go.uber.org/zap" + + plonk_bn254 "github.com/consensys/gnark/backend/plonk/bn254" + gnark_verifier_types "github.com/succinctlabs/gnark-plonky2-verifier/types" + "github.com/succinctlabs/gnark-plonky2-verifier/variables" + "github.com/succinctlabs/succinctx/gnarkx/types" +) + +type PlonkSystem struct { + logger *zap.Logger + circuitPath string + dataPath string +} + +func NewPlonkSystem(logger *zap.Logger, circuitPath string, dataPath string) *PlonkSystem { + return &PlonkSystem{ + logger: logger, + circuitPath: circuitPath, + dataPath: dataPath, + } +} + +func (s *PlonkSystem) Compile() error { + s.logger.Info("starting compiling verifier circuit") + + r1cs, pk, vk, err := s.CompileVerifierCircuit() + if err != nil { + return errors.Wrap(err, "compile verifier circuit") + } + + err = s.SaveVerifierCircuit(r1cs, pk, vk) + if err != nil { + return errors.Wrap(err, "save verifier circuit") + } + + s.logger.Info("successfully compiled verifier circuit") + + return nil +} + +func (s *PlonkSystem) Prove() error { + s.logger.Info("starting prove -- loading verifier circuit and proving key") + + // If the circuitPath is "" and not provided as part of the CLI flags, then we wait + // for user input. + if s.circuitPath == "" { + s.logger.Info("no circuitPath flag found, so user must input circuitPath via stdin") + reader := bufio.NewReader(os.Stdin) + str, err := reader.ReadString('\n') + if err != nil { + s.logger.Error("failed to parse the user provided circuitPath", zap.Error(err)) + } + trimmed := strings.TrimSuffix(str, "\n") + s.circuitPath = trimmed + } + + r1cs, err := s.LoadCircuit() + if err != nil { + return errors.Wrap(err, "load the verifier circuit") + } + pk, err := s.LoadProvingKey() + if err != nil { + return errors.Wrap(err, "load the proving key") + } + + _, _, err = s.ProveCircuit(r1cs, pk) + if err != nil { + return errors.Wrap(err, "create proof") + } + + s.logger.Info("successfully created proof") + + return nil +} + +func (s *PlonkSystem) Verify() error { + s.logger.Info("starting verify -- loading verifier key, public witness, and proof") + + vk, err := s.LoadVerifierKey() + if err != nil { + return errors.Wrap(err, "load verifier key") + } + + proof, err := s.LoadProof() + if err != nil { + return errors.Wrap(err, "load proof") + } + + publicWitness, err := s.LoadPublicWitness() + if err != nil { + return errors.Wrap(err, "load public witness") + } + + err = plonk.Verify(proof, vk, publicWitness) + if err != nil { + return errors.Wrap(err, "verify proof") + } + + s.logger.Info("successfully verified proof") + + return nil +} + +func (s *PlonkSystem) Export() error { + s.logger.Info("starting export -- loading verifier key and exporting Verifier solidity") + + vk, err := s.LoadVerifierKey() + if err != nil { + return errors.Wrap(err, "load verifier key") + } + + err = s.ExportVerifierSolidity(vk) + if err != nil { + return errors.Wrap(err, "export Verifier solidity") + } + + s.logger.Info("successfully exported Verifier solidity") + + return nil +} + +func (s *PlonkSystem) CompileVerifierCircuit() (constraint.ConstraintSystem, plonk.ProvingKey, plonk.VerifyingKey, error) { + verifierOnlyCircuitData := variables.DeserializeVerifierOnlyCircuitData( + gnark_verifier_types.ReadVerifierOnlyCircuitData(s.circuitPath + "/verifier_only_circuit_data.json"), + ) + proofWithPis := variables.DeserializeProofWithPublicInputs( + gnark_verifier_types.ReadProofWithPublicInputs(s.circuitPath + "/proof_with_public_inputs.json"), + ) + commonCircuitData := gnark_verifier_types.ReadCommonCircuitData(s.circuitPath + "/common_circuit_data.json") + + circuit := VerifierCircuit{ + ProofWithPis: proofWithPis, + VerifierData: verifierOnlyCircuitData, + VerifierDigest: new(frontend.Variable), + InputHash: new(frontend.Variable), + OutputHash: new(frontend.Variable), + CommonCircuitData: commonCircuitData, + } + r1cs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &circuit) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "compile verifier circuit") + } + + s.logger.Info("Running circuit setup") + start := time.Now() + srs, err := test.NewKZGSRS(r1cs) + if err != nil { + panic(err) + } + pk, vk, err := plonk.Setup(r1cs, srs) + if err != nil { + return nil, nil, nil, err + } + elapsed := time.Since(start) + s.logger.Info("Successfully ran circuit setup", zap.String("time", elapsed.String())) + + return r1cs, pk, vk, nil +} + +func (s *PlonkSystem) SaveVerifierCircuit(r1cs constraint.ConstraintSystem, pk plonk.ProvingKey, vk plonk.VerifyingKey) error { + os.MkdirAll(s.dataPath, 0755) + + r1csFile, err := os.Create(s.dataPath + "/r1cs.bin") + if err != nil { + return errors.Wrap(err, "create r1cs file") + } + r1cs.WriteTo(r1csFile) + r1csFile.Close() + s.logger.Debug("Successfully saved circuit constraints", zap.String("path", s.dataPath+"/r1cs.bin")) + + s.logger.Info("Saving proving key", zap.String("path", s.dataPath+"/pk.bin")) + pkFile, err := os.Create(s.dataPath + "/pk.bin") + if err != nil { + return errors.Wrap(err, "create pk file") + } + pk.WriteRawTo(pkFile) + pkFile.Close() + s.logger.Debug("Successfully saved proving key", zap.String("path", s.dataPath+"/pk.bin")) + + vkFile, err := os.Create(s.dataPath + "/vk.bin") + if err != nil { + return errors.Wrap(err, "create vk file") + } + vk.WriteRawTo(vkFile) + vkFile.Close() + s.logger.Debug("Successfully saved verifying key", zap.String("path", s.dataPath+"/vk.bin")) + + return nil +} + +func (s *PlonkSystem) ProveCircuit(r1cs constraint.ConstraintSystem, pk plonk.ProvingKey) (plonk.Proof, witness.Witness, error) { + verifierOnlyCircuitData := variables.DeserializeVerifierOnlyCircuitData( + gnark_verifier_types.ReadVerifierOnlyCircuitData(s.circuitPath + "/verifier_only_circuit_data.json"), + ) + proofWithPis := gnark_verifier_types.ReadProofWithPublicInputs(s.circuitPath + "/proof_with_public_inputs.json") + proofWithPisVariable := variables.DeserializeProofWithPublicInputs(proofWithPis) + + inputHash, outputHash := GetInputHashOutputHash(proofWithPis) + + // Circuit assignment + assignment := &VerifierCircuit{ + ProofWithPis: proofWithPisVariable, + VerifierData: verifierOnlyCircuitData, + VerifierDigest: verifierOnlyCircuitData.CircuitDigest, + InputHash: frontend.Variable(inputHash), + OutputHash: frontend.Variable(outputHash), + } + + s.logger.Debug("Generating witness") + start := time.Now() + witness, err := frontend.NewWitness(assignment, ecc.BN254.ScalarField()) + if err != nil { + return nil, nil, errors.Wrap(err, "generate witness") + } + elapsed := time.Since(start) + s.logger.Debug("Successfully generated witness", zap.Duration("time", elapsed)) + + s.logger.Debug("Creating proof") + start = time.Now() + proof, err := plonk.Prove(r1cs, pk, witness) + if err != nil { + return nil, nil, errors.Wrap(err, "create proof") + } + elapsed = time.Since(start) + s.logger.Info("Successfully created proof", zap.Duration("time", elapsed)) + + _proof := proof.(*plonk_bn254.Proof) + log.Info().Msg("Saving proof to proof.json") + jsonProof, err := json.Marshal(types.ProofResult{ + // Output will be filled in by plonky2x CLI + Output: []byte{}, + Proof: _proof.MarshalSolidity(), + }) + if err != nil { + return nil, nil, errors.Wrap(err, "marshal proof") + } + proofFile, err := os.Create("proof.json") + if err != nil { + return nil, nil, errors.Wrap(err, "create proof file") + } + defer proofFile.Close() + if _, err = proofFile.Write(jsonProof); err != nil { + return nil, nil, errors.Wrap(err, "write proof file") + } + s.logger.Info("Successfully saved proof") + + // Write proof with all the public inputs and save to disk. + jsonProofWithWitness, err := json.Marshal(struct { + InputHash hexutil.Bytes `json:"input_hash"` + OutputHash hexutil.Bytes `json:"output_hash"` + VerifierDigest hexutil.Bytes `json:"verifier_digest"` + Proof hexutil.Bytes `json:"proof"` + }{ + InputHash: inputHash.Bytes(), + OutputHash: outputHash.Bytes(), + VerifierDigest: (verifierOnlyCircuitData.CircuitDigest).(*big.Int).Bytes(), + Proof: _proof.MarshalSolidity(), + }) + if err != nil { + return nil, nil, errors.Wrap(err, "marshal proof with witness") + } + proofFile, err = os.Create("proof_with_witness.json") + if err != nil { + return nil, nil, errors.Wrap(err, "create proof_with_witness file") + } + defer proofFile.Close() + if _, err = proofFile.Write(jsonProofWithWitness); err != nil { + return nil, nil, errors.Wrap(err, "write proof_with_witness file") + } + s.logger.Info("Successfully saved proof_with_witness", zap.String("proofWithWitness", string(jsonProofWithWitness))) + + publicWitness, err := witness.Public() + if err != nil { + return nil, nil, errors.Wrap(err, "get public witness") + } + + s.logger.Info("Saving public witness to public_witness.bin") + witnessFile, err := os.Create("public_witness.bin") + if err != nil { + return nil, nil, errors.Wrap(err, "create public witness file") + } + defer witnessFile.Close() + if _, err = publicWitness.WriteTo(witnessFile); err != nil { + return nil, nil, errors.Wrap(err, "write public witness file") + } + s.logger.Info("Successfully saved public witness") + + return proof, publicWitness, nil +} + +func (s *PlonkSystem) ExportVerifierSolidity(vk plonk.VerifyingKey) error { + // Create a new buffer and export the VerifyingKey into it as a Solidity contract and + // convert the buffer content to a string for further manipulation. + buf := new(bytes.Buffer) + err := vk.ExportSolidity(buf) + if err != nil { + return errors.Wrap(err, "export verifying key to solidity") + } + content := buf.String() + + contractFile, err := os.Create(s.dataPath + "/Verifier.sol") + if err != nil { + return errors.Wrap(err, "create Verifier.sol file") + } + defer contractFile.Close() + + w := bufio.NewWriter(contractFile) + // write the new content to the writer + if _, err = w.Write([]byte(content)); err != nil { + return errors.Wrap(err, "write to Verifier.sol") + } + + if err = w.Flush(); err != nil { + return errors.Wrap(err, "flush writer for Verifier.sol") + } + + return nil +} + +func (s *PlonkSystem) LoadCircuit() (constraint.ConstraintSystem, error) { + r1cs := plonk.NewCS(ecc.BN254) + f, err := os.Open(s.dataPath + "/r1cs.bin") + if err != nil { + return nil, errors.Wrap(err, "open r1cs file") + } + r1csReader := bufio.NewReader(f) + _, err = r1cs.ReadFrom(r1csReader) + if err != nil { + return nil, errors.Wrap(err, "read r1cs file") + } + f.Close() + + return r1cs, nil +} + +func (s *PlonkSystem) LoadProvingKey() (pk plonk.ProvingKey, err error) { + pk = plonk.NewProvingKey(ecc.BN254) + f, err := os.Open(s.dataPath + "/pk.bin") + if err != nil { + return pk, errors.Wrap(err, "open pk file") + } + _, err = pk.ReadFrom(f) + if err != nil { + return pk, errors.Wrap(err, "read pk file") + } + f.Close() + + return pk, nil +} + +func (s *PlonkSystem) LoadVerifierKey() (vk plonk.VerifyingKey, err error) { + vk = plonk.NewVerifyingKey(ecc.BN254) + f, err := os.Open(s.dataPath + "/vk.bin") + if err != nil { + return nil, errors.Wrap(err, "open vk file") + } + _, err = vk.ReadFrom(f) + if err != nil { + return nil, errors.Wrap(err, "read vk file") + } + f.Close() + + return vk, nil +} + +func (s *PlonkSystem) LoadProof() (proof plonk.Proof, err error) { + proof = plonk.NewProof(ecc.BN254) + f, err := os.Open(s.dataPath + "/proof.json") + if err != nil { + return proof, errors.Wrap(err, "open proof file") + } + jsonProof, err := io.ReadAll(f) + if err != nil { + return proof, errors.Wrap(err, "read proof file") + } + err = json.Unmarshal(jsonProof, proof) + if err != nil { + return proof, errors.Wrap(err, "read proof file") + } + f.Close() + + return proof, nil +} + +func (s *PlonkSystem) LoadPublicWitness() (witness.Witness, error) { + publicWitness, err := witness.New(ecc.BN254.ScalarField()) + if err != nil { + return publicWitness, errors.Wrap(err, "create public witness") + } + f, err := os.Open(s.dataPath + "/public_witness.bin") + if err != nil { + return publicWitness, errors.Wrap(err, "open public witness file") + } + _, err = publicWitness.ReadFrom(f) + if err != nil { + return publicWitness, errors.Wrap(err, "read public witness file") + } + f.Close() + + return publicWitness, nil +} diff --git a/plonky2x/verifier/system/proof.go b/plonky2x/verifier/system/proof.go new file mode 100644 index 000000000..a2d05f268 --- /dev/null +++ b/plonky2x/verifier/system/proof.go @@ -0,0 +1,70 @@ +package system + +import ( + "encoding/json" + "fmt" + "math/big" + "os" + + "github.com/ethereum/go-ethereum/common/hexutil" + gnark_verifier_types "github.com/succinctlabs/gnark-plonky2-verifier/types" +) + +type ProofResult struct { + Proof hexutil.Bytes `json:"proof"` + Output hexutil.Bytes `json:"output"` +} + +type Groth16Proof struct { + A [2]*big.Int `json:"a"` + B [2][2]*big.Int `json:"b"` + C [2]*big.Int `json:"c"` + Input hexutil.Bytes `json:"input,omitempty"` + Output hexutil.Bytes `json:"output,omitempty"` +} + +// Export saves the proof to a file. +func (g *Groth16Proof) Export(file string) error { + // Write the proof to a JSON-compatible format. + + // Create the proof file. + proofFile, err := os.Create(file) + if err != nil { + panic(fmt.Errorf("failed to create file: %w", err)) + } + defer proofFile.Close() + + // Marshal the proof to JSON. + jsonString, err := json.Marshal(g) + if err != nil { + panic(fmt.Errorf("failed to marshal output: %w", err)) + } + + // Write the proof to the file. + _, err = proofFile.Write(jsonString) + if err != nil { + panic(fmt.Errorf("failed to write data: %w", err)) + } + + return nil +} + +func GetInputHashOutputHash(proofWithPis gnark_verifier_types.ProofWithPublicInputsRaw) (*big.Int, *big.Int) { + publicInputs := proofWithPis.PublicInputs + if len(publicInputs) != 64 { + panic("publicInputs must be 64 bytes") + } + publicInputsBytes := make([]byte, 64) + for i, v := range publicInputs { + publicInputsBytes[i] = byte(v & 0xFF) + } + inputHash := new(big.Int).SetBytes(publicInputsBytes[0:32]) + outputHash := new(big.Int).SetBytes(publicInputsBytes[32:64]) + if inputHash.BitLen() > 253 { + panic("inputHash must be at most 253 bits") + } + if outputHash.BitLen() > 253 { + panic("outputHash must be at most 253 bits") + } + return inputHash, outputHash +} diff --git a/plonky2x/verifier/verifier-build-prod/Verifier.sol b/plonky2x/verifier/verifier-build-prod/Verifier.sol new file mode 100644 index 000000000..48a53389e --- /dev/null +++ b/plonky2x/verifier/verifier-build-prod/Verifier.sol @@ -0,0 +1,546 @@ + +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/// @title Groth16 verifier template. +/// @author Remco Bloemen +/// @notice Supports verifying Groth16 proofs. Proofs can be in uncompressed +/// (256 bytes) and compressed (128 bytes) format. A view function is provided +/// to compress proofs. +/// @notice See for further explanation. +contract Verifier { + + /// Some of the provided public input values are larger than the field modulus. + /// @dev Public input elements are not automatically reduced, as this is can be + /// a dangerous source of bugs. + error PublicInputNotInField(); + + /// The proof is invalid. + /// @dev This can mean that provided Groth16 proof points are not on their + /// curves, that pairing equation fails, or that the proof is not for the + /// provided public input. + error ProofInvalid(); + + // Addresses of precompiles + uint256 constant PRECOMPILE_MODEXP = 0x05; + uint256 constant PRECOMPILE_ADD = 0x06; + uint256 constant PRECOMPILE_MUL = 0x07; + uint256 constant PRECOMPILE_VERIFY = 0x08; + + // Base field Fp order P and scalar field Fr order R. + // For BN254 these are computed as follows: + // t = 4965661367192848881 + // P = 36⋅t⁴ + 36⋅t³ + 24⋅t² + 6⋅t + 1 + // R = 36⋅t⁴ + 36⋅t³ + 18⋅t² + 6⋅t + 1 + uint256 constant P = 0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd47; + uint256 constant R = 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001; + + // Extension field Fp2 = Fp[i] / (i² + 1) + // Note: This is the complex extension field of Fp with i² = -1. + // Values in Fp2 are represented as a pair of Fp elements (a₀, a₁) as a₀ + a₁⋅i. + // Note: The order of Fp2 elements is *opposite* that of the pairing contract, which + // expects Fp2 elements in order (a₁, a₀). This is also the order in which + // Fp2 elements are encoded in the public interface as this became convention. + + // Constants in Fp + uint256 constant FRACTION_1_2_FP = 0x183227397098d014dc2822db40c0ac2ecbc0b548b438e5469e10460b6c3e7ea4; + uint256 constant FRACTION_27_82_FP = 0x2b149d40ceb8aaae81be18991be06ac3b5b4c5e559dbefa33267e6dc24a138e5; + uint256 constant FRACTION_3_82_FP = 0x2fcd3ac2a640a154eb23960892a85a68f031ca0c8344b23a577dcf1052b9e775; + + // Exponents for inversions and square roots mod P + uint256 constant EXP_INVERSE_FP = 0x30644E72E131A029B85045B68181585D97816A916871CA8D3C208C16D87CFD45; // P - 2 + uint256 constant EXP_SQRT_FP = 0xC19139CB84C680A6E14116DA060561765E05AA45A1C72A34F082305B61F3F52; // (P + 1) / 4; + + // Groth16 alpha point in G1 + uint256 constant ALPHA_X = 20491192805390485299153009773594534940189261866228447918068658471970481763042; + uint256 constant ALPHA_Y = 9383485363053290200918347156157836566562967994039712273449902621266178545958; + + // Groth16 beta point in G2 in powers of i + uint256 constant BETA_NEG_X_0 = 6375614351688725206403948262868962793625744043794305715222011528459656738731; + uint256 constant BETA_NEG_X_1 = 4252822878758300859123897981450591353533073413197771768651442665752259397132; + uint256 constant BETA_NEG_Y_0 = 11383000245469012944693504663162918391286475477077232690815866754273895001727; + uint256 constant BETA_NEG_Y_1 = 41207766310529818958173054109690360505148424997958324311878202295167071904; + + // Groth16 gamma point in G2 in powers of i + uint256 constant GAMMA_NEG_X_0 = 10857046999023057135944570762232829481370756359578518086990519993285655852781; + uint256 constant GAMMA_NEG_X_1 = 11559732032986387107991004021392285783925812861821192530917403151452391805634; + uint256 constant GAMMA_NEG_Y_0 = 13392588948715843804641432497768002650278120570034223513918757245338268106653; + uint256 constant GAMMA_NEG_Y_1 = 17805874995975841540914202342111839520379459829704422454583296818431106115052; + + // Groth16 delta point in G2 in powers of i + uint256 constant DELTA_NEG_X_0 = 2252026501241760122985686666960151054445999337435596171949785660558602071097; + uint256 constant DELTA_NEG_X_1 = 17992274670455051550992524951322548625420832725978979832621247888835898178116; + uint256 constant DELTA_NEG_Y_0 = 15231093211140501445645675296879596906442978621017789958128628670079320185645; + uint256 constant DELTA_NEG_Y_1 = 13850349638642800297413567762408968111004825613488465221677727347266947060629; + + // Constant and public input points + uint256 constant CONSTANT_X = 13855645236014131137949362561146818547345354074045498850362619693999820296042; + uint256 constant CONSTANT_Y = 12374273718945159465302748993739578965054171853134262391366612590757163335701; + uint256 constant PUB_0_X = 17334006822402461149913609155241629133790488515623047549658234759327913718395; + uint256 constant PUB_0_Y = 8225022999466227486808399132364595626060209746087507679814133498711113043662; + uint256 constant PUB_1_X = 4670066999597609028098141852902984938963663288737619214970382313975924395884; + uint256 constant PUB_1_Y = 21524848024563694141321818013348527657324448675452564896999501146788454436311; + uint256 constant PUB_2_X = 16500746611396891741656768868330795973337230082470739679216553280713595022142; + uint256 constant PUB_2_Y = 12339068396687699581694056867492749296120249334329649574771367535358109304484; + + /// Negation in Fp. + /// @notice Returns a number x such that a + x = 0 in Fp. + /// @notice The input does not need to be reduced. + /// @param a the base + /// @return x the result + function negate(uint256 a) internal pure returns (uint256 x) { + unchecked { + x = (P - (a % P)) % P; // Modulo is cheaper than branching + } + } + + /// Exponentiation in Fp. + /// @notice Returns a number x such that a ^ e = x in Fp. + /// @notice The input does not need to be reduced. + /// @param a the base + /// @param e the exponent + /// @return x the result + function exp(uint256 a, uint256 e) internal view returns (uint256 x) { + bool success; + assembly ("memory-safe") { + let f := mload(0x40) + mstore(f, 0x20) + mstore(add(f, 0x20), 0x20) + mstore(add(f, 0x40), 0x20) + mstore(add(f, 0x60), a) + mstore(add(f, 0x80), e) + mstore(add(f, 0xa0), P) + success := staticcall(gas(), PRECOMPILE_MODEXP, f, 0xc0, f, 0x20) + x := mload(f) + } + if (!success) { + // Exponentiation failed. + // Should not happen. + revert ProofInvalid(); + } + } + + /// Invertsion in Fp. + /// @notice Returns a number x such that a * x = 1 in Fp. + /// @notice The input does not need to be reduced. + /// @notice Reverts with ProofInvalid() if the inverse does not exist + /// @param a the input + /// @return x the solution + function invert_Fp(uint256 a) internal view returns (uint256 x) { + x = exp(a, EXP_INVERSE_FP); + if (mulmod(a, x, P) != 1) { + // Inverse does not exist. + // Can only happen during G2 point decompression. + revert ProofInvalid(); + } + } + + /// Square root in Fp. + /// @notice Returns a number x such that x * x = a in Fp. + /// @notice Will revert with InvalidProof() if the input is not a square + /// or not reduced. + /// @param a the square + /// @return x the solution + function sqrt_Fp(uint256 a) internal view returns (uint256 x) { + x = exp(a, EXP_SQRT_FP); + if (mulmod(x, x, P) != a) { + // Square root does not exist or a is not reduced. + // Happens when G1 point is not on curve. + revert ProofInvalid(); + } + } + + /// Square test in Fp. + /// @notice Returns wheter a number x exists such that x * x = a in Fp. + /// @notice Will revert with InvalidProof() if the input is not a square + /// or not reduced. + /// @param a the square + /// @return x the solution + function isSquare_Fp(uint256 a) internal view returns (bool) { + uint256 x = exp(a, EXP_SQRT_FP); + return mulmod(x, x, P) == a; + } + + /// Square root in Fp2. + /// @notice Fp2 is the complex extension Fp[i]/(i^2 + 1). The input is + /// a0 + a1 ⋅ i and the result is x0 + x1 ⋅ i. + /// @notice Will revert with InvalidProof() if + /// * the input is not a square, + /// * the hint is incorrect, or + /// * the input coefficents are not reduced. + /// @param a0 The real part of the input. + /// @param a1 The imaginary part of the input. + /// @param hint A hint which of two possible signs to pick in the equation. + /// @return x0 The real part of the square root. + /// @return x1 The imaginary part of the square root. + function sqrt_Fp2(uint256 a0, uint256 a1, bool hint) internal view returns (uint256 x0, uint256 x1) { + // If this square root reverts there is no solution in Fp2. + uint256 d = sqrt_Fp(addmod(mulmod(a0, a0, P), mulmod(a1, a1, P), P)); + if (hint) { + d = negate(d); + } + // If this square root reverts there is no solution in Fp2. + x0 = sqrt_Fp(mulmod(addmod(a0, d, P), FRACTION_1_2_FP, P)); + x1 = mulmod(a1, invert_Fp(mulmod(x0, 2, P)), P); + + // Check result to make sure we found a root. + // Note: this also fails if a0 or a1 is not reduced. + if (a0 != addmod(mulmod(x0, x0, P), negate(mulmod(x1, x1, P)), P) + || a1 != mulmod(2, mulmod(x0, x1, P), P)) { + revert ProofInvalid(); + } + } + + /// Compress a G1 point. + /// @notice Reverts with InvalidProof if the coordinates are not reduced + /// or if the point is not on the curve. + /// @notice The point at infinity is encoded as (0,0) and compressed to 0. + /// @param x The X coordinate in Fp. + /// @param y The Y coordinate in Fp. + /// @return c The compresed point (x with one signal bit). + function compress_g1(uint256 x, uint256 y) internal view returns (uint256 c) { + if (x >= P || y >= P) { + // G1 point not in field. + revert ProofInvalid(); + } + if (x == 0 && y == 0) { + // Point at infinity + return 0; + } + + // Note: sqrt_Fp reverts if there is no solution, i.e. the x coordinate is invalid. + uint256 y_pos = sqrt_Fp(addmod(mulmod(mulmod(x, x, P), x, P), 3, P)); + if (y == y_pos) { + return (x << 1) | 0; + } else if (y == negate(y_pos)) { + return (x << 1) | 1; + } else { + // G1 point not on curve. + revert ProofInvalid(); + } + } + + /// Decompress a G1 point. + /// @notice Reverts with InvalidProof if the input does not represent a valid point. + /// @notice The point at infinity is encoded as (0,0) and compressed to 0. + /// @param c The compresed point (x with one signal bit). + /// @return x The X coordinate in Fp. + /// @return y The Y coordinate in Fp. + function decompress_g1(uint256 c) internal view returns (uint256 x, uint256 y) { + // Note that X = 0 is not on the curve since 0³ + 3 = 3 is not a square. + // so we can use it to represent the point at infinity. + if (c == 0) { + // Point at infinity as encoded in EIP196 and EIP197. + return (0, 0); + } + bool negate_point = c & 1 == 1; + x = c >> 1; + if (x >= P) { + // G1 x coordinate not in field. + revert ProofInvalid(); + } + + // Note: (x³ + 3) is irreducible in Fp, so it can not be zero and therefore + // y can not be zero. + // Note: sqrt_Fp reverts if there is no solution, i.e. the point is not on the curve. + y = sqrt_Fp(addmod(mulmod(mulmod(x, x, P), x, P), 3, P)); + if (negate_point) { + y = negate(y); + } + } + + /// Compress a G2 point. + /// @notice Reverts with InvalidProof if the coefficients are not reduced + /// or if the point is not on the curve. + /// @notice The G2 curve is defined over the complex extension Fp[i]/(i^2 + 1) + /// with coordinates (x0 + x1 ⋅ i, y0 + y1 ⋅ i). + /// @notice The point at infinity is encoded as (0,0,0,0) and compressed to (0,0). + /// @param x0 The real part of the X coordinate. + /// @param x1 The imaginary poart of the X coordinate. + /// @param y0 The real part of the Y coordinate. + /// @param y1 The imaginary part of the Y coordinate. + /// @return c0 The first half of the compresed point (x0 with two signal bits). + /// @return c1 The second half of the compressed point (x1 unmodified). + function compress_g2(uint256 x0, uint256 x1, uint256 y0, uint256 y1) + internal view returns (uint256 c0, uint256 c1) { + if (x0 >= P || x1 >= P || y0 >= P || y1 >= P) { + // G2 point not in field. + revert ProofInvalid(); + } + if ((x0 | x1 | y0 | y1) == 0) { + // Point at infinity + return (0, 0); + } + + // Compute y^2 + // Note: shadowing variables and scoping to avoid stack-to-deep. + uint256 y0_pos; + uint256 y1_pos; + { + uint256 n3ab = mulmod(mulmod(x0, x1, P), P-3, P); + uint256 a_3 = mulmod(mulmod(x0, x0, P), x0, P); + uint256 b_3 = mulmod(mulmod(x1, x1, P), x1, P); + y0_pos = addmod(FRACTION_27_82_FP, addmod(a_3, mulmod(n3ab, x1, P), P), P); + y1_pos = negate(addmod(FRACTION_3_82_FP, addmod(b_3, mulmod(n3ab, x0, P), P), P)); + } + + // Determine hint bit + // If this sqrt fails the x coordinate is not on the curve. + bool hint; + { + uint256 d = sqrt_Fp(addmod(mulmod(y0_pos, y0_pos, P), mulmod(y1_pos, y1_pos, P), P)); + hint = !isSquare_Fp(mulmod(addmod(y0_pos, d, P), FRACTION_1_2_FP, P)); + } + + // Recover y + (y0_pos, y1_pos) = sqrt_Fp2(y0_pos, y1_pos, hint); + if (y0 == y0_pos && y1 == y1_pos) { + c0 = (x0 << 2) | (hint ? 2 : 0) | 0; + c1 = x1; + } else if (y0 == negate(y0_pos) && y1 == negate(y1_pos)) { + c0 = (x0 << 2) | (hint ? 2 : 0) | 1; + c1 = x1; + } else { + // G1 point not on curve. + revert ProofInvalid(); + } + } + + /// Decompress a G2 point. + /// @notice Reverts with InvalidProof if the input does not represent a valid point. + /// @notice The G2 curve is defined over the complex extension Fp[i]/(i^2 + 1) + /// with coordinates (x0 + x1 ⋅ i, y0 + y1 ⋅ i). + /// @notice The point at infinity is encoded as (0,0,0,0) and compressed to (0,0). + /// @param c0 The first half of the compresed point (x0 with two signal bits). + /// @param c1 The second half of the compressed point (x1 unmodified). + /// @return x0 The real part of the X coordinate. + /// @return x1 The imaginary poart of the X coordinate. + /// @return y0 The real part of the Y coordinate. + /// @return y1 The imaginary part of the Y coordinate. + function decompress_g2(uint256 c0, uint256 c1) + internal view returns (uint256 x0, uint256 x1, uint256 y0, uint256 y1) { + // Note that X = (0, 0) is not on the curve since 0³ + 3/(9 + i) is not a square. + // so we can use it to represent the point at infinity. + if (c0 == 0 && c1 == 0) { + // Point at infinity as encoded in EIP197. + return (0, 0, 0, 0); + } + bool negate_point = c0 & 1 == 1; + bool hint = c0 & 2 == 2; + x0 = c0 >> 2; + x1 = c1; + if (x0 >= P || x1 >= P) { + // G2 x0 or x1 coefficient not in field. + revert ProofInvalid(); + } + + uint256 n3ab = mulmod(mulmod(x0, x1, P), P-3, P); + uint256 a_3 = mulmod(mulmod(x0, x0, P), x0, P); + uint256 b_3 = mulmod(mulmod(x1, x1, P), x1, P); + + y0 = addmod(FRACTION_27_82_FP, addmod(a_3, mulmod(n3ab, x1, P), P), P); + y1 = negate(addmod(FRACTION_3_82_FP, addmod(b_3, mulmod(n3ab, x0, P), P), P)); + + // Note: sqrt_Fp2 reverts if there is no solution, i.e. the point is not on the curve. + // Note: (X³ + 3/(9 + i)) is irreducible in Fp2, so y can not be zero. + // But y0 or y1 may still independently be zero. + (y0, y1) = sqrt_Fp2(y0, y1, hint); + if (negate_point) { + y0 = negate(y0); + y1 = negate(y1); + } + } + + /// Compute the public input linear combination. + /// @notice Reverts with PublicInputNotInField if the input is not in the field. + /// @notice Computes the multi-scalar-multiplication of the public input + /// elements and the verification key including the constant term. + /// @param input The public inputs. These are elements of the scalar field Fr. + /// @return x The X coordinate of the resulting G1 point. + /// @return y The Y coordinate of the resulting G1 point. + function publicInputMSM(uint256[3] calldata input) + internal view returns (uint256 x, uint256 y) { + // Note: The ECMUL precompile does not reject unreduced values, so we check this. + // Note: Unrolling this loop does not cost much extra in code-size, the bulk of the + // code-size is in the PUB_ constants. + // ECMUL has input (x, y, scalar) and output (x', y'). + // ECADD has input (x1, y1, x2, y2) and output (x', y'). + // We call them such that ecmul output is already in the second point + // argument to ECADD so we can have a tight loop. + bool success = true; + assembly ("memory-safe") { + let f := mload(0x40) + let g := add(f, 0x40) + let s + mstore(f, CONSTANT_X) + mstore(add(f, 0x20), CONSTANT_Y) + mstore(g, PUB_0_X) + mstore(add(g, 0x20), PUB_0_Y) + s := calldataload(input) + mstore(add(g, 0x40), s) + success := and(success, lt(s, R)) + success := and(success, staticcall(gas(), PRECOMPILE_MUL, g, 0x60, g, 0x40)) + success := and(success, staticcall(gas(), PRECOMPILE_ADD, f, 0x80, f, 0x40)) + mstore(g, PUB_1_X) + mstore(add(g, 0x20), PUB_1_Y) + s := calldataload(add(input, 32)) + mstore(add(g, 0x40), s) + success := and(success, lt(s, R)) + success := and(success, staticcall(gas(), PRECOMPILE_MUL, g, 0x60, g, 0x40)) + success := and(success, staticcall(gas(), PRECOMPILE_ADD, f, 0x80, f, 0x40)) + mstore(g, PUB_2_X) + mstore(add(g, 0x20), PUB_2_Y) + s := calldataload(add(input, 64)) + mstore(add(g, 0x40), s) + success := and(success, lt(s, R)) + success := and(success, staticcall(gas(), PRECOMPILE_MUL, g, 0x60, g, 0x40)) + success := and(success, staticcall(gas(), PRECOMPILE_ADD, f, 0x80, f, 0x40)) + x := mload(f) + y := mload(add(f, 0x20)) + } + if (!success) { + // Either Public input not in field, or verification key invalid. + // We assume the contract is correctly generated, so the verification key is valid. + revert PublicInputNotInField(); + } + } + + /// Compress a proof. + /// @notice Will revert with InvalidProof if the curve points are invalid, + /// but does not verify the proof itself. + /// @param proof The uncompressed Groth16 proof. Elements are in the same order as for + /// verifyProof. I.e. Groth16 points (A, B, C) encoded as in EIP-197. + /// @return compressed The compressed proof. Elements are in the same order as for + /// verifyCompressedProof. I.e. points (A, B, C) in compressed format. + function compressProof(uint256[8] calldata proof) + public view returns (uint256[4] memory compressed) { + compressed[0] = compress_g1(proof[0], proof[1]); + (compressed[2], compressed[1]) = compress_g2(proof[3], proof[2], proof[5], proof[4]); + compressed[3] = compress_g1(proof[6], proof[7]); + } + + /// Verify a Groth16 proof with compressed points. + /// @notice Reverts with InvalidProof if the proof is invalid or + /// with PublicInputNotInField the public input is not reduced. + /// @notice There is no return value. If the function does not revert, the + /// proof was successfully verified. + /// @param compressedProof the points (A, B, C) in compressed format + /// matching the output of compressProof. + /// @param input the public input field elements in the scalar field Fr. + /// Elements must be reduced. + function verifyCompressedProof( + uint256[4] calldata compressedProof, + uint256[3] calldata input + ) public view { + (uint256 Ax, uint256 Ay) = decompress_g1(compressedProof[0]); + (uint256 Bx0, uint256 Bx1, uint256 By0, uint256 By1) = decompress_g2( + compressedProof[2], compressedProof[1]); + (uint256 Cx, uint256 Cy) = decompress_g1(compressedProof[3]); + (uint256 Lx, uint256 Ly) = publicInputMSM(input); + + // Verify the pairing + // Note: The precompile expects the F2 coefficients in big-endian order. + // Note: The pairing precompile rejects unreduced values, so we won't check that here. + uint256[24] memory pairings; + // e(A, B) + pairings[ 0] = Ax; + pairings[ 1] = Ay; + pairings[ 2] = Bx1; + pairings[ 3] = Bx0; + pairings[ 4] = By1; + pairings[ 5] = By0; + // e(C, -δ) + pairings[ 6] = Cx; + pairings[ 7] = Cy; + pairings[ 8] = DELTA_NEG_X_1; + pairings[ 9] = DELTA_NEG_X_0; + pairings[10] = DELTA_NEG_Y_1; + pairings[11] = DELTA_NEG_Y_0; + // e(α, -β) + pairings[12] = ALPHA_X; + pairings[13] = ALPHA_Y; + pairings[14] = BETA_NEG_X_1; + pairings[15] = BETA_NEG_X_0; + pairings[16] = BETA_NEG_Y_1; + pairings[17] = BETA_NEG_Y_0; + // e(L_pub, -γ) + pairings[18] = Lx; + pairings[19] = Ly; + pairings[20] = GAMMA_NEG_X_1; + pairings[21] = GAMMA_NEG_X_0; + pairings[22] = GAMMA_NEG_Y_1; + pairings[23] = GAMMA_NEG_Y_0; + + // Check pairing equation. + bool success; + uint256[1] memory output; + assembly ("memory-safe") { + success := staticcall(gas(), PRECOMPILE_VERIFY, pairings, 0x300, output, 0x20) + } + if (!success || output[0] != 1) { + // Either proof or verification key invalid. + // We assume the contract is correctly generated, so the verification key is valid. + revert ProofInvalid(); + } + } + + /// Verify an uncompressed Groth16 proof. + /// @notice Reverts with InvalidProof if the proof is invalid or + /// with PublicInputNotInField the public input is not reduced. + /// @notice There is no return value. If the function does not revert, the + /// proof was successfully verified. + /// @param proof the points (A, B, C) in EIP-197 format matching the output + /// of compressProof. + /// @param input the public input field elements in the scalar field Fr. + /// Elements must be reduced. + function verifyProof( + uint256[8] calldata proof, + uint256[3] calldata input + ) public view { + (uint256 x, uint256 y) = publicInputMSM(input); + + // Note: The precompile expects the F2 coefficients in big-endian order. + // Note: The pairing precompile rejects unreduced values, so we won't check that here. + + bool success; + assembly ("memory-safe") { + let f := mload(0x40) // Free memory pointer. + + // Copy points (A, B, C) to memory. They are already in correct encoding. + // This is pairing e(A, B) and G1 of e(C, -δ). + calldatacopy(f, proof, 0x100) + + // Complete e(C, -δ) and write e(α, -β), e(L_pub, -γ) to memory. + // OPT: This could be better done using a single codecopy, but + // Solidity (unlike standalone Yul) doesn't provide a way to + // to do this. + mstore(add(f, 0x100), DELTA_NEG_X_1) + mstore(add(f, 0x120), DELTA_NEG_X_0) + mstore(add(f, 0x140), DELTA_NEG_Y_1) + mstore(add(f, 0x160), DELTA_NEG_Y_0) + mstore(add(f, 0x180), ALPHA_X) + mstore(add(f, 0x1a0), ALPHA_Y) + mstore(add(f, 0x1c0), BETA_NEG_X_1) + mstore(add(f, 0x1e0), BETA_NEG_X_0) + mstore(add(f, 0x200), BETA_NEG_Y_1) + mstore(add(f, 0x220), BETA_NEG_Y_0) + mstore(add(f, 0x240), x) + mstore(add(f, 0x260), y) + mstore(add(f, 0x280), GAMMA_NEG_X_1) + mstore(add(f, 0x2a0), GAMMA_NEG_X_0) + mstore(add(f, 0x2c0), GAMMA_NEG_Y_1) + mstore(add(f, 0x2e0), GAMMA_NEG_Y_0) + + // Check pairing equation. + success := staticcall(gas(), PRECOMPILE_VERIFY, f, 0x300, f, 0x20) + // Also check returned value (both are either 1 or 0). + success := and(success, mload(f)) + } + if (!success) { + // Either proof or verification key invalid. + // We assume the contract is correctly generated, so the verification key is valid. + revert ProofInvalid(); + } + } +} diff --git a/plonky2x/verifier/verifier.go b/plonky2x/verifier/verifier.go deleted file mode 100644 index 79c1ea474..000000000 --- a/plonky2x/verifier/verifier.go +++ /dev/null @@ -1,101 +0,0 @@ -package main - -import ( - "bufio" - "bytes" - "encoding/json" - "fmt" - "io" - "os" - "time" - - "github.com/consensys/gnark-crypto/ecc" - "github.com/consensys/gnark/backend/groth16" - groth16Bn254 "github.com/consensys/gnark/backend/groth16/bn254" - "github.com/consensys/gnark/backend/witness" - "github.com/consensys/gnark/logger" -) - -func LoadVerifierKey(path string) (groth16.VerifyingKey, error) { - log := logger.Logger() - vkFile, err := os.Open(path + "/vk.bin") - if err != nil { - return nil, fmt.Errorf("failed to open vk file: %w", err) - } - vk := groth16.NewVerifyingKey(ecc.BN254) - start := time.Now() - _, err = readV08VerifyingKey(vk.(*groth16Bn254.VerifyingKey), vkFile) - if err != nil { - return nil, fmt.Errorf("failed to read vk file: %w", err) - } - vkFile.Close() - elapsed := time.Since(start) - log.Debug().Msg("Successfully loaded verifying key, time: " + elapsed.String()) - - return vk, nil -} - -func LoadPublicWitness(circuitPath string) (witness.Witness, error) { - log := logger.Logger() - witnessFile, err := os.Open(circuitPath + "/public_witness.bin") - if err != nil { - return nil, fmt.Errorf("failed to open public witness file: %w", err) - } - publicWitness, err := witness.New(ecc.BN254.ScalarField()) - if err != nil { - return nil, fmt.Errorf("failed to create public witness: %w", err) - } - publicWitness.ReadFrom(witnessFile) - witnessFile.Close() - log.Debug().Msg("Successfully loaded public witness") - - return publicWitness, nil -} - -func LoadProof() (groth16.Proof, error) { - log := logger.Logger() - proofFile, err := os.Open("/proof.json") - if err != nil { - return nil, fmt.Errorf("failed to open proof file: %w", err) - } - proof := groth16.NewProof(ecc.BN254) - jsonProof, err := io.ReadAll(proofFile) - if err != nil { - return nil, fmt.Errorf("failed to read proof file: %w", err) - } - err = json.Unmarshal(jsonProof, proof) - if err != nil { - return nil, fmt.Errorf("failed to read proof file: %w", err) - } - proofFile.Close() - log.Debug().Msg("Successfully loaded proof") - - return proof, nil -} - -func ExportIFunctionVerifierSolidity(path string, vk groth16.VerifyingKey) error { - log := logger.Logger() - // Create a new buffer and export the VerifyingKey into it as a Solidity contract and - // convert the buffer content to a string for further manipulation. - buf := new(bytes.Buffer) - err := vk.ExportSolidity(buf) - if err != nil { - log.Err(err).Msg("failed to export verifying key to solidity") - return err - } - content := buf.String() - - contractFile, err := os.Create(path + "/Verifier.sol") - if err != nil { - return err - } - w := bufio.NewWriter(contractFile) - // write the new content to the writer - _, err = w.Write([]byte(content)) - if err != nil { - return err - } - - contractFile.Close() - return err -} diff --git a/plonky2x/verifier/verifier_test.go b/plonky2x/verifier/verifier_test.go index 8d36b77eb..fdc4cfd05 100644 --- a/plonky2x/verifier/verifier_test.go +++ b/plonky2x/verifier/verifier_test.go @@ -27,7 +27,7 @@ func TestPlonky2xVerifierCircuit(t *testing.T) { ) commonCircuitDataDummy := types.ReadCommonCircuitData(dummyCircuitPath + "/common_circuit_data.json") - circuit := Plonky2xVerifierCircuit{ + circuit := VerifierCircuit{ ProofWithPis: proofWithPisDummy, VerifierData: verifierOnlyCircuitDataDummy, VerifierDigest: frontend.Variable(0), // Can be empty for defining the circuit @@ -44,7 +44,7 @@ func TestPlonky2xVerifierCircuit(t *testing.T) { proofWithPisVariable := variables.DeserializeProofWithPublicInputs(proofWithPis) - witness := Plonky2xVerifierCircuit{ + witness := VerifierCircuit{ ProofWithPis: proofWithPisVariable, VerifierData: verifierOnlyCircuitData, VerifierDigest: verifierOnlyCircuitData.CircuitDigest,