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

Jan 11, 2025 - 05:29
 0
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 <== 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 :)