Niftyzk tutorials 3 - Edwards-curve Digital Signatures
Niftyzk supports EdDSA which is a public cryptography digital signature scheme.This tutorial will contain information about the generated code and you will need to read the previous tutorials to understand everything. Let’s start with niftyzk init Setting up your current directory ? What project do you want to scaffold? EdDSA signature verification ? Choose the hashing algorithm to use: mimc7 ? Do you wish to add public inputs to sign? (E.g: address,amount) yes ? Enter the name of the public inputs in a comma separated list (no numbers or special characters): address,amount Generating circuits Generating javascript Done So we selected EdDSA with mimc7 and added some inputs to sign, address, amount Using MiMC7 hash means we will have to use a key to create hashes which is kept secret. However for signing with EdDSA using MiMC, the key is not used. That decision to not use the key for signing was made by the developers of circomlib, as using a default key is not an issue, the signed data is a public hash. Let’s see the generated circuits, you see there is a circuit.circom file pragma circom 2.0.0; include "../node_modules/circomlib/circuits/eddsamimc.circom"; include "../node_modules/circomlib/circuits/mimc.circom"; template VerifySignature(){ signal input message; signal input address; signal input amount; signal input k; signal input Ax; signal input Ay; signal input S; signal input R8x; signal input R8y; component eddsa = EdDSAMiMCVerifier(); component mimc7Hash = MultiMiMC7(3, 91); mimc7Hash.k
Niftyzk supports EdDSA which is a public cryptography digital signature scheme.This tutorial will contain information about the generated code and you will need to read the previous tutorials to understand everything.
Let’s start with niftyzk init
Setting up your current directory
? What project do you want to scaffold? EdDSA signature verification
? Choose the hashing algorithm to use: mimc7
? Do you wish to add public inputs to sign? (E.g: address,amount) yes
? Enter the name of the public inputs in a comma separated list (no numbers or special characters): address,amount
Generating circuits
Generating javascript
Done
So we selected EdDSA with mimc7 and added some inputs to sign, address, amount
Using MiMC7 hash means we will have to use a key to create hashes which is kept secret. However for signing with EdDSA using MiMC, the key is not used. That decision to not use the key for signing was made by the developers of circomlib, as using a default key is not an issue, the signed data is a public hash.
Let’s see the generated circuits, you see there is a circuit.circom
file
pragma circom 2.0.0;
include "../node_modules/circomlib/circuits/eddsamimc.circom";
include "../node_modules/circomlib/circuits/mimc.circom";
template VerifySignature(){
signal input message;
signal input address;
signal input amount;
signal input k;
signal input Ax;
signal input Ay;
signal input S;
signal input R8x;
signal input R8y;
component eddsa = EdDSAMiMCVerifier();
component mimc7Hash = MultiMiMC7(3, 91);
mimc7Hash.k <== k;
mimc7Hash.in[0] <== message;
mimc7Hash.in[1] <== address;
mimc7Hash.in[2] <== amount;
eddsa.enabled <== 1;
eddsa.Ax <== Ax;
eddsa.Ay <== Ay;
eddsa.S <== S;
eddsa.R8x <== R8x;
eddsa.R8y <== R8y;
eddsa.M <== mimc7Hash.out;
}
component main {public [message,address,amount]} = VerifySignature();
So we import the required dependencies, we use the same hashing algorithm for the signatures and the message hash.
So we got a message
input, address
and amount
which were extra and these are our public inputs that will be revealed on-chain for verification.
k
is the secret used for the mimc hashing
Ax
,Ay
are the points used for the public key, S
, R8x
,R8y
are signature parameters returned by the signing function.
We compute a MiMC7 hash using k
and then just assign the inputs to the eddsa
template input signals. By specifying eddsa.enabled <== 1;
we assert that the signature must be valid for the hash.Now let’s look at some javascript
Accounts are created using EdDSA:
/**
* The signature parameters used for verifying pedersen hash signed EdDSA
* @typedef {Object} Account
* @property {Buffer} prvKey - Private key buffer
* @property {Uint8Array[]} pubKey - Public key is a tuple of 32 bit Uint8Array
*/
/**
* @param {any} eddsa - The EdDSA object
* @returns {Account}
* Generate a new account which composes of a private and public key
*/
export function generateAccount(eddsa) {
const prvKey = rbytes();
const pubKey = eddsa.prv2pub(prvKey);
return {
prvKey,
pubKey
}
}
We use 32 bytes for the account, the rbytes()
returns a random buffer. It uses crypto.randomBytes(32)
The public keys are a tuple of Uint8Array, 32 bits in size each.
/**
*
* @param {Array} pubKey - Used for computing an address
* @param {bigin | number} key -The key used for the mimc7 hash
* @returns
*/
export async function getAddressFromPubkey(pubKey, key) {
return mimc7(pubKey, key);
}
We compute “addresses” from the public key by hashing it. These addresses are not valid blockchain addresses but usable for account abstraction. You can derive addresses from a public key using different schemes and derivation paths. It’s up to you what you choose.
/**
* @typedef {Object} Message
* @property {string | bigint} message
* @property {string | bigint} address
* @property {string | bigint} amount
*/
/**
*
* @param {Message} data - The data content of the message. The hash of the data object will be signed
* @param key {number | bigint} - The key used for the mimc7 hash
* @returns {bigint} Returns a mimc7 hash
*/
export async function computeMessageHash(data, key) {
return await mimc7(
[
BigInt(data.message),
BigInt(data.address),
BigInt(data.amount)
], key)
}
The message hash is computed using mimc7, it’s the same hashing that happens inside the circuit for verification.
/**
* @param {any} eddsa - the built EDDSA
* @param {bigint} messagehash - The poseidon hash of the message
* @param {Buffer} prvKey - The private key used to sign the message
* @returns {Signature} signature
*/
export function signMessage(eddsa, messageHash, prvKey) {
const signature = eddsa.signMiMC(prvKey, eddsa.F.e(messageHash));
const pubKey = eddsa.prv2pub(prvKey);
assert(eddsa.verifyMiMC(eddsa.F.e(messageHash), signature, pubKey))
return {
signature,
pubKey
}
}
/**
* @typedef {Object} Signature
* @property {Uint8Array[]} R8
* @property {bigint} S
* /
Message signing uses the above function. You pass an eddsa object, the computed message hash and the private key created for your account. You can see the type of the signature defined with JsDoc.
/**
* @typedef {Object} SignatureParameters
* @property {bigint} Ax
* @property {bigint} Ay
* @property {bigint} R8x
* @property {bigint} R8y
* @property {bigint} S
*/
/**
* @param {any} eddsa
* @param {Uint8Array[]} pubKey - The public key of the account
* @param {Signature} signature - The signature of the signed message
* @returns {SignatureParameters} - The signature parameters are prepared parameters, ready to use for the circuit
*/
export function getSignatureParameters(eddsa, pubKey, signature) {
return {
Ax: eddsa.F.toObject(pubKey[0]),
Ay: eddsa.F.toObject(pubKey[1]),
R8x: eddsa.F.toObject(signature.R8[0]),
R8y: eddsa.F.toObject(signature.R8[1]),
S: signature.S
}
}
Now to use this signature in circom, we need to convert it. So we get the signature parameters for verification with the above function. /test/input.js
/**
* This is a test input, generated for the starting circuit.
* If you update the inputs, you need to update this function to match it.
*/
export async function getInput(){
await buildHashImplementation()
const eddsa = await getEDDSA();
const account = generateAccount(eddsa);
const key = 313; // just an example key for tests
const message = rbigint();
const address = rbigint()
const amount = rbigint()
const messageHash = await computeMessageHash({message,address,amount}, key)
const signedMessage = signMessage(eddsa, messageHash, account.prvKey);
const signatureParameters = getSignatureParameters(eddsa, account.pubKey, signedMessage.signature)
return {
Ax: signatureParameters.Ax,
Ay: signatureParameters.Ay,
S: signatureParameters.S,
R8x: signatureParameters.R8x,
R8y: signatureParameters.R8y,
k: key,
address,
amount,
message
}
}
And above you can see how we compute the input for the circuits. it’s used for the hot reload and the tests
Now I’ll show you how the merkle trees work with EdDSA
Setting up your current directory
? What project do you want to scaffold? EdDSA signature verification with Fixed Merkle Tree
? Choose the hashing algorithm to use: poseidon
? Do you wish to add public inputs to sign? (E.g: address,amount) no
So regenerated the project using niftyzk init
and selected different parameters.And this is the circuit.circom
file now:
pragma circom 2.0.0;
include "../node_modules/circomlib/circuits/eddsaposeidon.circom";
include "../node_modules/circomlib/circuits/poseidon.circom";
include "./merkletree.circom";
template VerifySignature(levels){
signal input message;
signal input root;
signal input pathIndices[levels];
signal input pathElements[levels];
//The parameters for the signature
//The Ax and Ay parameters are the public key, Ax = pubKey[0], Ay = pubKey[1]
signal input Ax;
signal input Ay;
signal input S;
signal input R8x;
signal input R8y;
component eddsa = EdDSAPoseidonVerifier();
component poseidon = Poseidon(1);
poseidon.inputs[0] <== message;
// Verify the signature on the message hash
eddsa.enabled <== 1;
eddsa.Ax <== Ax;
eddsa.Ay <== Ay;
eddsa.S <== S;
eddsa.R8x <== R8x;
eddsa.R8y <== R8y;
eddsa.M <== poseidon.out;
//We compute a public key by hashing Ax and Ay,
// this will be later used with the merkle tree
component pubKeyHasher = Poseidon(2);
pubKeyHasher.inputs[0] <== Ax;
pubKeyHasher.inputs[1] <== Ay;
//Verify the merkle tree
component tree = MerkleTreeChecker(levels);
for (var i = 0; i < levels; i++) {
tree.pathElements[i] <== pathElements[i];
tree.pathIndices[i] <== pathIndices[i];
}
tree.root <== root;
tree.leaf <== pubKeyHasher.out;
}
component main {public [message,root]} = VerifySignature(20);
So you can see there are some differences. I didn’t add more extra parameters to sign this time, so there is only two public inputs, message
and root
.
So root
is the merkle tree root we going to verify that the public key that signed this message is contained inside the tree. It’s a way of access control, so we know that the signature is valid and the signer public key is allowed to pass the verification because his key is inside the tree.
We compute the address using the pubKeyHasher
from Ax and Ay, you can see it does exactly the same as the Javascript code I showed earlier. The MerkleTreeChecker
asserts correctness and will fail if the leaf is not contained in the root.
pathIndices
and pathElements
are the merkle proof. You can explore the merkle tree circuit in the file merkletree.circom
I explained it in a previous tutorial.
So to manually create merkle trees using the cli, we can use npm run new
that will generate addresses and create a tree using them as leaves.
CREATING A NEW MERKLE TREE
Enter how many accounts would you like to generate:
4
Generating accounts and addresses. Please wait!
Done
Generating merkle tree from addresses!
Done.Root is 21804813178957116268623645093306988559564490743632413729059072914509024611553
Serializing data.
Writing to file.
Done
The private keys for the corresponding accounts are placed in the private
directory while the rest is placed in the public
directory. If you chose MiMC7
or MiMCSponge
hashes then the key parameter is saved in the private dir and needed for verification, otherwise you don’t need to use the private details to create a merkle proof and verify it.
so let’s run npm run proof
and it will ask for a root and an address and spit out a merkle proof
then npm run verify
will ask for a root and the proof and if eveything is running correctly the proof will be valid.
You can use EdDSA for Identity, Rollups, Scaling,Voting or Account Abstraction. It’s up to you :)