From c39cc8dc3a24fc533e96d178f70bbc43a57097e6 Mon Sep 17 00:00:00 2001 From: Ivo Kubjas Date: Wed, 6 Aug 2025 10:24:39 +0000 Subject: [PATCH 1/4] docs: add public key hashing example --- examples/pubkeyhashing/doc.go | 7 + examples/pubkeyhashing/pubkeyhashing_test.go | 153 +++++++++++++++++++ 2 files changed, 160 insertions(+) create mode 100644 examples/pubkeyhashing/doc.go create mode 100644 examples/pubkeyhashing/pubkeyhashing_test.go diff --git a/examples/pubkeyhashing/doc.go b/examples/pubkeyhashing/doc.go new file mode 100644 index 0000000000..8a7b45ec31 --- /dev/null +++ b/examples/pubkeyhashing/doc.go @@ -0,0 +1,7 @@ +// Package pubkeyhashing implements a simple example of ECDSA public key hashing using SHA2. +// +// This example demonstrates how we can verify ECDSA signature in a circuit and +// compare that the hash of the public key matches the expected hash. It also +// illustrates how to minimize the public inputs by packing hash into two +// 16-byte variables to fit into the BN254 field. +package pubkeyhashing diff --git a/examples/pubkeyhashing/pubkeyhashing_test.go b/examples/pubkeyhashing/pubkeyhashing_test.go new file mode 100644 index 0000000000..1f7e73dab7 --- /dev/null +++ b/examples/pubkeyhashing/pubkeyhashing_test.go @@ -0,0 +1,153 @@ +package pubkeyhashing + +import ( + "crypto/rand" + "crypto/sha256" + "fmt" + "math/big" + + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/std/algebra/emulated/sw_emulated" + "github.com/consensys/gnark/std/conversion" + "github.com/consensys/gnark/std/hash/sha2" + "github.com/consensys/gnark/std/math/emulated" + "github.com/consensys/gnark/std/math/emulated/emparams" + "github.com/consensys/gnark/std/math/uints" + "github.com/consensys/gnark/std/signature/ecdsa" + "github.com/consensys/gnark/test" + + "github.com/consensys/gnark-crypto/ecc" + p256_ecdsa "github.com/consensys/gnark-crypto/ecc/secp256k1/ecdsa" +) + +// PubkeySHA2 is a circuit that verifies ECDSA signature and checks that the +// hash of the public key matches the expected hash. +// +// The fields of the struct define the public and private inputs to the circuit. +// The actual circuit is defined in the [PubKeySHA2.Define] method. +type PubKeySHA2 struct { + // PublicKeyHash is 32 bytes, but we split it into two 16-byte variables to fit into BN254 field + PublicKeyHash [2]frontend.Variable `gnark:",public"` + Signature ecdsa.Signature[emparams.Secp256k1Fr] `gnark:",public"` + Msg emulated.Element[emparams.Secp256k1Fr] // if tag is not set, then it is a private input + PublicKey ecdsa.PublicKey[emparams.Secp256k1Fp, emparams.Secp256k1Fr] // actual public key is also a private input +} + +func (c *PubKeySHA2) Define(api frontend.API) error { + // -- hash the given public key + // - first we convert the public key coordinates to bytes + xbytes, err := conversion.EmulatedToBytes(api, &c.PublicKey.X) + if err != nil { + return fmt.Errorf("failed to convert PublicKey.X to bytes: %w", err) + } + ybytes, err := conversion.EmulatedToBytes(api, &c.PublicKey.Y) + if err != nil { + return fmt.Errorf("failed to convert PublicKey.Y to bytes: %w", err) + } + // - now we compute the SHA2 hash of the concatenated bytes + h, err := sha2.New(api) + if err != nil { + return fmt.Errorf("failed to create SHA2 instance: %w", err) + } + h.Write(xbytes) + h.Write(ybytes) + // - and compute the hash + computedHash := h.Sum() + // -- now we check that the computed hash matches the expected hash + // - first, we used [2]frontend.Variable to store the hash so that we wouldn't be using too much public inputs and we want the parts to fit into BN254 field, so 16-byte chunks + // we convert it back to bytes + var hashpubkeybytes []uints.U8 + for i := range c.PublicKeyHash { + bts, err := conversion.NativeToBytes(api, c.PublicKeyHash[i]) + if err != nil { + return fmt.Errorf("failed to convert PublicKeyHash[%d] to bytes: %w", i, err) + } + // NativeToBytes returns 32 bytes (MSB order), but we set only 16 bytes so take the last 16 bytes + hashpubkeybytes = append(hashpubkeybytes, bts[16:]...) + } + // - now we need to initialize bytes gadget for comparison + bapi, err := uints.NewBytes(api) + if err != nil { + return fmt.Errorf("failed to create bytes gadget: %w", err) + } + if len(hashpubkeybytes) != len(computedHash) { + return fmt.Errorf("hashpubkeybytes and computedHash have different lengths: %d vs %d", len(hashpubkeybytes), len(computedHash)) + } + // - finally we check that the computed hash matches the expected hash + for i := range hashpubkeybytes { + bapi.AssertIsEqual(hashpubkeybytes[i], computedHash[i]) + } + + // -- now we check that the signature is valid + c.PublicKey.Verify(api, sw_emulated.GetCurveParams[emparams.Secp256k1Fp](), &c.Msg, &c.Signature) + + return nil +} + +func Example() { + // generate random key pair + sk, err := p256_ecdsa.GenerateKey(rand.Reader) + if err != nil { + panic(fmt.Sprintf("failed to generate key: %v", err)) + } + pubkey := sk.PublicKey + + // compute the hash of the public key + h := sha256.New() + h.Write(pubkey.Bytes()) + pubHash := h.Sum(nil) + pubHashLo := pubHash[:16] + pubHashHi := pubHash[16:32] + + msg := []byte("this is a test message for pubkey hashing!") + // obtain the signature + sig, err := sk.Sign(msg, sha256.New()) + if err != nil { + panic(fmt.Sprintf("failed to sign message: %v", err)) + } + // sanity check + ok, err := pubkey.Verify(sig, msg, sha256.New()) + if err != nil { + panic(fmt.Sprintf("failed to verify signature: %v", err)) + } + if !ok { + panic("signature verification failed") + } + + // the signature has concatenated R and S values. Lets unwrap them + var sigT p256_ecdsa.Signature + _, err = sigT.SetBytes(sig) + if err != nil { + panic(fmt.Sprintf("failed to set bytes for signature: %v", err)) + } + r, s := new(big.Int), new(big.Int) + r.SetBytes(sigT.R[:32]) + s.SetBytes(sigT.S[:32]) + + // compute the hash of the message as an integer + mshHash := sha256.Sum256(msg) + msgHashInt := p256_ecdsa.HashToInt(mshHash[:]) + + // now we prepare the witness for the circuit + assignment := &PubKeySHA2{ + // we splitted the public key hash into two 16-byte variables to fit into BN254 field + PublicKeyHash: [2]frontend.Variable{pubHashLo, pubHashHi}, + // we construct the public key as non-native element. NB! this means that both X and Y coordinates are 4 limbs of 64 bytes each, so 8 limbs total + PublicKey: ecdsa.PublicKey[emparams.Secp256k1Fp, emparams.Secp256k1Fr]{ + X: emulated.ValueOf[emulated.Secp256k1Fp](pubkey.A.X), + Y: emulated.ValueOf[emulated.Secp256k1Fp](pubkey.A.Y), + }, + Signature: ecdsa.Signature[emparams.Secp256k1Fr]{ + R: emulated.ValueOf[emparams.Secp256k1Fr](r), + S: emulated.ValueOf[emparams.Secp256k1Fr](s), + }, + Msg: emulated.ValueOf[emparams.Secp256k1Fr](msgHashInt), + } + + // we use a test solver for checking that the circuit is solved correctly. For creating actual SNARK proofs, use either Groth16 or PLONK backends. + err = test.IsSolved(&PubKeySHA2{}, assignment, ecc.BN254.ScalarField()) + if err != nil { + panic(fmt.Sprintf("failed to solve the circuit: %v", err)) + } + // Output: +} From 550dda29accb3ef3b6586312f2828a652dc3f5a4 Mon Sep 17 00:00:00 2001 From: Ivo Kubjas Date: Wed, 6 Aug 2025 10:24:55 +0000 Subject: [PATCH 2/4] docs: add witness vector example --- examples/witness/doc.go | 7 +++ examples/witness/witness_test.go | 83 ++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 examples/witness/doc.go create mode 100644 examples/witness/witness_test.go diff --git a/examples/witness/doc.go b/examples/witness/doc.go new file mode 100644 index 0000000000..87248af97a --- /dev/null +++ b/examples/witness/doc.go @@ -0,0 +1,7 @@ +// Package witness provides an example of witness export as vector. +// +// gnark abstracts away the creation of the witness vector from assignment, but +// in some cases it is useful to have it as a vector (for example, to compare +// values against known state). This example shows how to create a witness +// vector from assignment and access individual values in it. +package witness diff --git a/examples/witness/witness_test.go b/examples/witness/witness_test.go new file mode 100644 index 0000000000..65a8e77f37 --- /dev/null +++ b/examples/witness/witness_test.go @@ -0,0 +1,83 @@ +package witness + +import ( + "fmt" + + "github.com/consensys/gnark-crypto/ecc" + "github.com/consensys/gnark-crypto/ecc/bn254" + fr_bn254 "github.com/consensys/gnark-crypto/ecc/bn254/fr" + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/cs/r1cs" + "github.com/consensys/gnark/std/algebra/emulated/sw_bn254" + "github.com/consensys/gnark/std/algebra/emulated/sw_emulated" +) + +type Circuit struct { + A frontend.Variable `gnark:",public"` + B frontend.Variable + P sw_emulated.AffinePoint[sw_bn254.BaseField] `gnark:",public"` +} + +func (c *Circuit) Define(api frontend.API) error { + api.AssertIsDifferent(c.A, c.B) + + curve, err := sw_bn254.NewPairing(api) + if err != nil { + return err + } + curve.AssertIsOnG1(&c.P) + return nil +} + +func Example() { + ccs, err := frontend.Compile(ecc.BN254.ScalarField(), r1cs.NewBuilder, &Circuit{}) + if err != nil { + panic(fmt.Sprintf("failed to compile the circuit: %v", err)) + } + + a, b := 3, 4 + _, _, P, _ := bn254.Generators() + fmt.Printf("assignment: A = %d, B = %d, P = %s\n", a, b, P.String()) + assignment := &Circuit{A: a, B: b, P: sw_bn254.NewG1Affine(P)} + witness, err := frontend.NewWitness(assignment, ecc.BN254.ScalarField()) + if err != nil { + panic(fmt.Sprintf("failed to create witness: %v", err)) + } + _, err = ccs.Solve(witness) + if err != nil { + panic(fmt.Sprintf("failed to solve the circuit: %v", err)) + } + witnessVector := witness.Vector().(fr_bn254.Vector) + for i, v := range witnessVector { + fmt.Printf("witness[%d] = %s\n", i, v.String()) + } + pubWitness, err := witness.Public() + if err != nil { + panic(fmt.Sprintf("failed to get public witness: %v", err)) + } + pubWitnessVector := pubWitness.Vector().(fr_bn254.Vector) + for i, v := range pubWitnessVector { + fmt.Printf("public witness[%d] = %s\n", i, v.String()) + } + // Output: + // assignment: A = 3, B = 4, P = E([1,2]) + // witness[0] = 3 + // witness[1] = 1 + // witness[2] = 0 + // witness[3] = 0 + // witness[4] = 0 + // witness[5] = 2 + // witness[6] = 0 + // witness[7] = 0 + // witness[8] = 0 + // witness[9] = 4 + // public witness[0] = 3 + // public witness[1] = 1 + // public witness[2] = 0 + // public witness[3] = 0 + // public witness[4] = 0 + // public witness[5] = 2 + // public witness[6] = 0 + // public witness[7] = 0 + // public witness[8] = 0 +} From 484cbce03fbec4919f26a675af11f981e299db8c Mon Sep 17 00:00:00 2001 From: Ivo Kubjas Date: Wed, 6 Aug 2025 10:25:08 +0000 Subject: [PATCH 3/4] docs: fix input packing example package doc --- examples/inputpacking/doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/inputpacking/doc.go b/examples/inputpacking/doc.go index a95ed0400f..b3b0be3c29 100644 --- a/examples/inputpacking/doc.go +++ b/examples/inputpacking/doc.go @@ -1,5 +1,5 @@ // Package inputpacking illustrates input packing for reducing public input. - +// // Usually in a SNARK circuit there are public and private inputs. The public // inputs are known to the prover and verifier, while the private inputs are // known only to the prover. To verify the proof, the verifier needs to provide From 866632a8b10815948c383c5cb6fcc977d3f1593c Mon Sep 17 00:00:00 2001 From: Ivo Kubjas Date: Wed, 6 Aug 2025 10:25:26 +0000 Subject: [PATCH 4/4] docs: add documentation for examples pkg --- examples/doc.go | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 examples/doc.go diff --git a/examples/doc.go b/examples/doc.go new file mode 100644 index 0000000000..d9839ffb29 --- /dev/null +++ b/examples/doc.go @@ -0,0 +1,2 @@ +// Package examples provides various example circuits. +package examples