EdDSA
This tutorial verifies an EdDSA signature inside a zk-SNARK circuit. It is useful in zk-rollup-style applications where an operator proves that a batch of user signatures is valid.
EdDSA in gnark uses twisted Edwards curves defined over the SNARK field. These companion curves are commonly known as JubJub for BLS12-381 and Baby JubJub for BN254. Using a companion curve avoids the high cost of emulating an external curve inside the native SNARK field.
Write the circuit
The circuit carries the public key, signature, and message. An untagged curveID field selects the companion curve:
type eddsaCircuit struct {
curveID tedwards.ID
PublicKey eddsa.PublicKey `gnark:",public"`
Signature eddsa.Signature `gnark:",public"`
Message frontend.Variable `gnark:",public"`
}
Define constructs the companion curve and a MiMC hasher, then delegates verification to the maintained eddsa gadget:
func (circuit *eddsaCircuit) Define(api frontend.API) error {
curve, err := twistededwards.NewEdCurve(api, circuit.curveID)
if err != nil {
return err
}
hFunc, err := mimc.NewMiMC(api)
if err != nil {
return err
}
return eddsa.Verify(
curve,
circuit.Signature,
circuit.Message,
circuit.PublicKey,
&hFunc,
)
}
The gadget verifies that the scalar is in the group order, performs the required double-base scalar multiplication, checks the resulting point, applies the cofactor, and asserts the verification equation.
Generate an out-of-circuit signature
Generate the key and signature with gnark-crypto. For BN254, use the matching MiMC hasher:
randomness := rand.New(rand.NewSource(seed)) //#nosec G404 -- deterministic tutorial input
privateKey, err := eddsa.New(tedwards.BN254, randomness)
if err != nil {
return err
}
hFunc := hash.MIMC_BN254.New()
msg := []byte{0xde, 0xad, 0xf0, 0x0d}
signature, err := privateKey.Sign(msg, hFunc)
if err != nil {
return err
}
publicKey := privateKey.Public()
valid, err := publicKey.Verify(signature, msg, hFunc)
if err != nil {
return err
}
if !valid {
return errors.New("out-of-circuit verification failed")
}
Assign and test the circuit
The Assign helpers decode the compressed public key and signature into circuit-friendly values:
var circuit eddsaCircuit
circuit.curveID = tedwards.BN254
var assignment eddsaCircuit
assignment.curveID = tedwards.BN254
assignment.Message = msg
assignment.PublicKey.Assign(tedwards.BN254, publicKey.Bytes())
assignment.Signature.Assign(tedwards.BN254, signature)
assert := test.NewAssert(t)
assert.CheckCircuit(&circuit,
test.WithValidAssignment(&assignment),
test.WithCurves(ecc.BN254),
)
You can also add an invalid assignment, for example the same signature with a different message:
var invalidAssignment eddsaCircuit
invalidAssignment.curveID = tedwards.BN254
invalidAssignment.Message = []byte{0xde, 0xad, 0xf0, 0x0e}
invalidAssignment.PublicKey.Assign(tedwards.BN254, publicKey.Bytes())
invalidAssignment.Signature.Assign(tedwards.BN254, signature)
assert.CheckCircuit(&circuit,
test.WithValidAssignment(&assignment),
test.WithInvalidAssignment(&invalidAssignment),
test.WithCurves(ecc.BN254),
)
Prove and verify
Compile, create witnesses, and run Groth16:
ccs, err := frontend.Compile(
ecc.BN254.ScalarField(),
r1cs.NewBuilder,
&circuit,
)
if err != nil {
return err
}
witness, err := frontend.NewWitness(&assignment, ecc.BN254.ScalarField())
if err != nil {
return err
}
publicWitness, err := witness.Public()
if err != nil {
return err
}
pk, vk, err := groth16.Setup(ccs)
if err != nil {
return err
}
proof, err := groth16.Prove(ccs, pk, witness)
if err != nil {
return err
}
return groth16.Verify(proof, vk, publicWitness)