Emails serve as our gateway to various online services and platforms. ZK Email uses zk proofs to authenticate these interactions on chain while preserving privacy.
This guide will walk you through setting up a circuit that enables you to verify your Twitter username on-chain, without disclosing any information beyond the username itself. This process uses ZK Email libraries and regex verification to ensure the authenticity and privacy of your Twitter username, allowing you to mint an NFT upon completion.
Before diving into the technical details, we encourage you to experience Proof of Twitter firsthand. Visit https://twitter.prove.email/ to interact with a user-friendly interface that showcases the functionality of this technology. This hands-on experience will provide you with a practical understanding of the concepts we'll explore in this guide, making the process of building your own circuit more intuitive.
The ZK Email library contains 3 packages.
zk-email/helpers - helper functions to help generate and verify the inputs of the circuit
zk-email/contracts - contains the DKIM Registry for public domains (google, yahoo, etc)
zk-email/circuits - circuit templates that are used in creating your circuit
Create a new folder and initiate your repository:
yarn init -y
Install all three packages by running:
yarn add @zk-email/circuits @zk-email/helpers @zk-email/contracts
To verify ownership of a Twitter account, we will use the "Forgot your password" email from Twitter. This email inherently includes the username and can be generated at any time. By authenticating this email, we can confirm that the user has control over the corresponding Twitter account.
Let's start by obtaining the raw email file:
Initiate a password reset process on Twitter to send yourself a reset email.
Locate the email from Twitter in your inbox and download its headers. If you're using gmail you can find this option under a menu represented by three dots, then select 'Download Message'. You can also do 'Show Original' then 'Copy to Clipboard'.
For different email clients, the process varies slightly:
If you're using Outlook, switch to plain text mode. Then, copy and paste the 'full email details' into the textbox on the client-side webpage.
For Gmail and Yahoo users, select 'Download Original Message', then copy and paste the contents into the textbox.
Now, let's generate the input for a zkregex using zkregex.com. This tool will help us create a regular expression (regex) that matches the pattern of a Twitter username in the email body.
Go to zkregex.com.
Enter the regex for a Twitter username in the "Regex Pattern" field. We'll use @([a-zA-Z0-9_]+).
Input example Twitter usernames in the "Input 1", "Input 2", and "Input 3" fields, like @yush_g
, @zkemail
, and @elonMusk
.
Click "Test" to generate and test the Circom and Halo2 circuits.
In the "Select which states to be revealed" step, choose the states that correspond to the Twitter username.
Click "Generate" to get the Circom code for your circuit.
We will integrate the regex circuit generated into our primary circuit. This will allow us to demonstrate that a user possesses an email authenticated by Twitter's private key. We can verify the presence of a string in the email body that corresponds to the user's Twitter handle and publicly disclose the username as part of the proof.
After setting up the main Twitter verification circuit, we can proceed to generate inputs for your circuit using the ZK Email helpers SDK.
You can create a inputs.ts
file and place this code inside:
import { bytesToBigInt, fromHex } from "@zk-email/helpers";
import { generateCircuitInputs } from "@zk-email/helpers";
import { verifyDKIMSignature } from "@zk-email/helpers"
import fs from "fs"
import path from "path"
export const STRING_PRESELECTOR = "email was meant for @";
export const MAX_HEADER_PADDED_BYTES = 1024;
export const MAX_BODY_PADDED_BYTES = 1536;
export async function generateTwitterVerifierCircuitInputs() {
const rawEmail = fs.readFileSync(
path.join(__dirname, "./emls/rawEmail.eml"),
"utf8"
);
const dkimResult = await verifyDKIMSignature(Buffer.from(rawEmail));
const emailVerifierInputs = generateCircuitInputs({
rsaSignature: dkimResult.signature,
rsaPublicKey: dkimResult.publicKey,
body: dkimResult.body,
bodyHash:dkimResult.bodyHash,
message: dkimResult.message,
shaPrecomputeSelector: STRING_PRESELECTOR,
maxMessageLength: MAX_HEADER_PADDED_BYTES,
maxBodyLength: MAX_BODY_PADDED_BYTES
});
const bodyRemaining = emailVerifierInputs.in_body_padded!.map(c => Number(c));
const selectorBuffer = Buffer.from(STRING_PRESELECTOR);
const usernameIndex = Buffer.from(bodyRemaining).indexOf(selectorBuffer) + selectorBuffer.length;
const address = bytesToBigInt(fromHex("0x71C7656EC7ab88b098defB751B7401B5f6d897")).toString();
const inputJson = {
...emailVerifierInputs,
twitter_username_idx: usernameIndex.toString(),
address,
};
fs.writeFileSync("./input.json", JSON.stringify(inputJson))
}
(async () => {
await generateTwitterVerifierCircuitInputs();
}) ();
To create the inputs.json file run:
npx ts-node inputs.ts
This script reads your raw email file, verifies the DKIM signature, generates the circuit inputs, and writes them to an input.json file.
This input.json file will be used later for witness generation and proving.
The generateTwitterVerifierCircuitInputs function is the main function that does all the work. It uses helper functions from the @zk-email/helpers package to generate the inputs and write them to a file.
Remember to replace the address and the path to the raw email file with your own wallet address.
To set up your Twitter circuit, follow these steps:
To build your circom circuit, you need to import two files. The first one is the email-verifier.circom
from our library. The second one is the twitter_reset_regex.circom
file that you generated earlier in this tutorial.
include "@zk-email/circuits/email-verifier.circom";
include "/twitter_reset_regex.circom";
The email-verifier.circom
file validates each of the inputs to the circuit, while the twitter_reset_regex.circom
file scans the email body for the '@username', ensuring it adheres to the specified regex definition.
template TwitterVerifier(max_header_bytes, max_body_bytes, n, k, pack_size) {}
In this step, we start the circuit template with the following arguments:
max_header_bytes = 1024
- This is the maximum number of bytes in the header.
max_body_bytes = 1536
- This is the maximum number of bytes in the body after the precomputed slice.
n = 121
- This is the number of bits in each chunk of the pubkey (RSA parameter).
k = 17
- This is the number of chunks in the pubkey (RSA parameter). Note that 121 * 17 > 2048.
pack_size = 31
- This is the number of bytes that can fit into a 255ish bit signal (can increase later).
signal input in_padded[max_header_bytes];
signal input pubkey[k];
signal input signature[k];
signal input in_len_padded_bytes;
signal input address;
signal input body_hash_idx;
signal input precomputed_sha[32];
signal input in_body_padded[max_body_bytes];
signal input in_body_len_padded_bytes;
signal input twitter_username_idx;
signal output pubkey_hash;
signal output reveal_twitter_packed[max_twitter_packed_bytes];
The circuit's inputs are all private unless explicitly stated otherwise, while outputs are always public. In this example, we have several inputs and outputs.
Inputs
in_padded[max_header_bytes]
- Prehashed email data.
pubkey[k]
- RSA public key.
signature[k]
- RSA signature.
in_len_padded_bytes
- Length of the email, including padding.
address
- Wallet address.
body_hash_idx
- Index of the body hash.
twitter_username_idx
- Index of twitter username.
Outputs
pubkey_hash
- Hash of the public key, cross-referenced with the public key in our DKIM registry to authenticate the origins of the email.
reveal_twitter_packed
- Packed Twitter username that has been verified and is ready for reveal.
This part of the code defines the EmailVerifier component and assigns the necessary inputs to it. The EmailVerifier
is a crucial part of the circuit as it validates the email inputs.
component EV = EmailVerifier(max_header_bytes, max_body_bytes, n, k, 0);
EV.in_padded <== in_padded;
EV.pubkey <== pubkey;
EV.signature <== signature;
EV.in_len_padded_bytes <== in_len_padded_bytes;
EV.body_hash_idx <== body_hash_idx;
EV.precomputed_sha <== precomputed_sha;
EV.in_body_padded <== in_body_padded;
EV.in_body_len_padded_bytes <== in_body_len_padded_bytes;
pubkey_hash <== EV.pubkey_hash;
In this part of the code, we will add the code to verify the Twitter username that is found in the email body.
We start by defining the maximum length of a Twitter username and calculating the maximum number of bytes it can occupy when packed.
var max_twitter_len = 21;
var max_twitter_packed_bytes = count_packed(max_twitter_len, pack_size);
Next, we use the TwitterResetRegex
function to compute the regex states on each character in the email body. This function is part of the circom code we generated earlier. It ensures that at least one match is found in the email body, i.e., the match count is not zero.
signal (twitter_regex_out, twitter_regex_reveal[max_body_bytes]) <== TwitterResetRegex(max_body_bytes)(in_body_padded);
signal is_found_twitter <== IsZero()(twitter_regex_out);
is_found_twitter === 0;
Finally, we pack the Twitter username for output, into a minimal fixed number of bytes using the ShiftAndPackMaskedStr
function. This packing process combines 31 bytes into a single 255 bit circom signal, and the shift process isolates the match and moves it to the start of the output. It is necessary to ensure that the username fits into the minimal output signal size.
reveal_twitter_packed <== ShiftAndPackMaskedStr(max_body_bytes, max_twitter_len, pack_size)(twitter_regex_reveal, twitter_username_idx);
For the finished circuit, refer to the following code:
To understand the contracts on-chain that verify this proof, you can peek at the smart contracts as well.
After setting up your Twitter circuit, the next step is to compile it and compute the witness. This process involves generating the verification key (vkey) and the zk-SNARK proving keys (zkeys).
To compile the circuit locally, you need to have Rust and Circom installed first. You can visit this link to install both: https://docs.circom.io/getting-started/installation/#installing-dependencies
circom -l node_modules TwitterVerifier.circom -o --r1cs --wasm --sym --c
This process will generate a r1cs
file, a wasm
file, and a sym
file. These files are essential for generating the vkey, zkey, and computing the witness.
Now, let's move on to computing the witness. The role of the witness is to ensure that the circuit's signals adhere to the defined constraints.
Execute the following command in your terminal:
node generate_witness.js twitterverifier.wasm input.json witness.wtns
For in-browser proving, we will generate chunked zkeys. This is because the twitter.circom file is quite large, leading to extended proving times.
To begin, install this specific version of snarkjs which supports chunked key verification.
npm install snarkjs@git+https://github.com/vb7401/snarkjs.git#24981febe8826b6ab76ae4d76cf7f9142919d2b8
In case you're performing server-side proving, you can install the standard version of snarkjs.
npm install -g snarkjs
The process of generating proving and verification keys for a zk-SNARK circuit involves several steps.
powersOfTau28_hez_final_22.ptau
file from the provided URL. This file is necessary for the zkey new
command to generate the initial .zkey
file with zero contributions.bash
wget https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28hez_final22.ptau
snarkjs powersoftau beacon pot22_0001.ptau pot22_beacon.ptau 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f 10 -n='Final Beacon'
snarkjs powersoftau prepare phase2 pot22_beacon.ptau pot22_final.ptau -v
snarkjs zkey new pot22_final.ptau twitterverifier.wasm twitter_0000.zkey
snarkjs zkey contribute twitter_0000.zkey twitter_0001.zkey --name='1st Contributor Name' -v
snarkjs zkey beacon twitter_0001.zkey twitter_final.zkey 0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f 10 -n='Final Beacon'
snarkjs zkey export verificationkey twitter_final.zkey
Now, you have the verification_key.json
file which can be used to verify proofs off-chain.
To create a Solidity verifier that allows you to verify your proof on the Ethereum blockchain, run:
snarkjs zkey export solidityverifier circuit_final.zkey verifier.sol
This will generate a verifier.sol
contract that is used to validate the proof on chain.
The ProofOfTwitter.sol
contract includes the required on-chain logic for verifying Twitter accounts. When the proof is validated successfully, an NFT is minted on the blockchain.
The contract begins by importing two crucial files: Dkimregistry.sol
and Verifier.sol
.
import { Verifier } from "./Verifier.sol";
import "@zk-email/contracts/DKIMRegistry.sol";
Dkimregistry.sol
maintains a registry of public domains. This registry is used to cross-verify the domain from which the email originated in the proof.
Verifier.sol
contract is responsible for validating the proof.
It then defines constants that represent the indices of various public signals within the proof, such as the DKIM public key hash, the Twitter username, and the Ethereum address. These constants are used to retrieve the corresponding values from the signals array during verification.
uint32 public constant pubKeyHashIndexInSignals = 0; // index of DKIM public key hash in signals array
uint32 public constant usernameIndexInSignals = 1; // index of first packed twitter username in signals array
uint32 public constant usernameLengthInSignals = 1; // length of packed twitter username in signals array
uint32 public constant addressIndexInSignals = 2; // index of ethereum address in signals array
The contract includes a function to verify the DKIM public key hash against a registry to ensure the email's authenticity:
bytes32 dkimPublicKeyHashInCircuit = bytes32(signals[pubKeyHashIndexInSignals]);
require(dkimRegistry.isDKIMPublicKeyHashValid(domain, dkimPublicKeyHashInCircuit), "invalid dkim signature");
It also contains a function that uses the verifier
contract to check the validity of the provided proof:
require(
verifier.verifyProof(
[proof[0], proof[1]],
[[proof[2], proof[3]], [proof[4], proof[5]]],
[proof[6], proof[7]],
signals
),
"Invalid Proof"
);
Additionally, the contract includes logic to extract the Twitter username from the packed signals array. Although currently, a Twitter username can fit within a single signal, the code is designed to handle longer usernames that may span multiple signals:
uint256[] memory usernamePack = new uint256[](usernameLengthInSignals);
for (uint256 i = usernameIndexInSignals; i < (usernameIndexInSignals + usernameLengthInSignals); i++) {
usernamePack[i - usernameIndexInSignals] = signals[i];
}
Understanding these components is essential for anyone looking to build a system that leverages this contract for Twitter account verification using zk-SNARKs. The full contract code is available at the GitHub repository: ProofOfTwitter.sol
This guide provided just one example of a circuit you can build. For more examples and resources, you can check out the zk-email verifier usage guide and the Twitter Circuit Code.
This is just the beginning of verifiable provenance data. As you continue to explore and build, you'll discover the vast potential of this technology. The power of zk-SNARKs allows for the creation of systems where data can be verified without revealing any additional information. This opens up a world of possibilities for privacy-preserving applications.