feat: added CleanEmailAddress template

This commit is contained in:
shreyas-londhe
2025-02-06 13:23:04 +05:30
parent e1084969fb
commit dcf6d7b76f
3 changed files with 256 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
import { wasm as wasm_tester } from "circom_tester";
import path from "path";
describe("CleanEmailAddress", () => {
jest.setTimeout(20_1000);
let circuit: any;
beforeAll(async () => {
circuit = await wasm_tester(
path.join(
__dirname,
"./test-circuits/clean-email-address-test.circom"
),
{
recompile: true,
include: path.join(__dirname, "../../../node_modules"),
output: path.join(__dirname, "./compiled-test-circuits"),
}
);
});
it("should correctly clean email with dots and alias", async () => {
const input = {
encoded: stringToAsciiArray("shreyas.londhe+alias@gmail.com", 32),
decoded: stringToAsciiArray("shreyaslondhe@gmail.com", 32),
};
const witness = await circuit.calculateWitness(input);
await circuit.checkConstraints(witness);
await circuit.assertOut(witness, { isValid: 1 });
});
it("should fail when decoded email is incorrect", async () => {
const input = {
encoded: stringToAsciiArray("shreyas.londhe+alias@gmail.com", 32),
decoded: stringToAsciiArray("shreyaslondhe@yahoo.com", 32), // wrong domain
};
const witness = await circuit.calculateWitness(input);
await circuit.checkConstraints(witness);
await circuit.assertOut(witness, { isValid: 0 });
});
it("should handle email with only dots", async () => {
const input = {
encoded: stringToAsciiArray("shreyas.londhe@gmail.com", 32),
decoded: stringToAsciiArray("shreyaslondhe@gmail.com", 32),
};
const witness = await circuit.calculateWitness(input);
await circuit.checkConstraints(witness);
await circuit.assertOut(witness, { isValid: 1 });
});
it("should handle email with only alias", async () => {
const input = {
encoded: stringToAsciiArray("shreyaslondhe+test@gmail.com", 32),
decoded: stringToAsciiArray("shreyaslondhe@gmail.com", 32),
};
const witness = await circuit.calculateWitness(input);
await circuit.checkConstraints(witness);
await circuit.assertOut(witness, { isValid: 1 });
});
it("should handle email with multiple dots", async () => {
const input = {
encoded: stringToAsciiArray("shreyas.londhe.test@gmail.com", 32),
decoded: stringToAsciiArray("shreyaslondhetest@gmail.com", 32),
};
const witness = await circuit.calculateWitness(input);
await circuit.checkConstraints(witness);
await circuit.assertOut(witness, { isValid: 1 });
});
it("should handle email with complex alias", async () => {
const input = {
encoded: stringToAsciiArray(
"shs.loe+test.alias+123@gmail.com",
32
),
decoded: stringToAsciiArray("shsloe@gmail.com", 32),
};
const witness = await circuit.calculateWitness(input);
await circuit.checkConstraints(witness);
await circuit.assertOut(witness, { isValid: 1 });
});
it("should handle clean email with no modifications needed", async () => {
const input = {
encoded: stringToAsciiArray("shreyaslondhe@gmail.com", 32),
decoded: stringToAsciiArray("shreyaslondhe@gmail.com", 32),
};
const witness = await circuit.calculateWitness(input);
await circuit.checkConstraints(witness);
await circuit.assertOut(witness, { isValid: 1 });
});
});
// Helper function to convert string to ASCII array with padding
function stringToAsciiArray(str: string, length: number): number[] {
const result = new Array(length).fill(0);
for (let i = 0; i < str.length; i++) {
result[i] = str.charCodeAt(i);
}
return result;
}

View File

@@ -0,0 +1,5 @@
pragma circom 2.1.6;
include "../../utils/email.circom";
component main = CleanEmailAddress(32);

View File

@@ -0,0 +1,140 @@
pragma circom 2.1.6;
include "circomlib/circuits/comparators.circom";
include "circomlib/circuits/gates.circom";
include "circomlib/circuits/mux1.circom";
include "./hash.circom";
/// @title CleanEmailAddress
/// @notice Verifies that a cleaned email address is derived correctly from the original
/// @notice Assumes that the encoded email address is valid
/// @dev Removes periods and plus aliases from email addresses
/// @param maxLength Maximum length of the email address
/// @input encoded Original email address (e.g., "shreyas.londhe+alias@gmail.com")
/// @input decoded Cleaned email address (e.g., "shreyaslondhe@gmail.com")
/// @output isValid 1 if decoded is valid cleaned version of encoded
template CleanEmailAddress(maxLength) {
signal input encoded[maxLength];
signal input decoded[maxLength];
signal output isValid;
// Helper signals
signal r;
signal isPlus[maxLength];
signal isAt[maxLength];
signal isPlusAndAt[maxLength];
signal isLocalPart[maxLength];
signal isPeriod[maxLength];
signal isLocalPeriod[maxLength];
signal foundPlus[maxLength];
signal hasAliasPart;
signal afterPlus[maxLength];
signal afterAt[maxLength];
signal inAliasPartTemp[maxLength];
signal inAliasPart[maxLength];
signal shouldRemove[maxLength];
signal processed[maxLength];
signal rEnc[maxLength];
signal rDec[maxLength];
signal sumEnc[maxLength];
signal sumDec[maxLength];
// Helper components
component muxEnc[maxLength];
component rHasher = PoseidonModular(2 * maxLength);
// Generate random value r using Poseidon
for (var i = 0; i < maxLength; i++) {
rHasher.in[i] <== encoded[i];
rHasher.in[maxLength + i] <== decoded[i];
}
r <== rHasher.out;
// Check for '+' (43 in ASCII)
// Check for '@' (64 in ASCII)
isPlus[0] <== IsEqual()([encoded[0], 43]);
isAt[0] <== IsEqual()([encoded[0], 64]);
isPlusAndAt[0] <== (1 - isPlus[0]) * (1 - isAt[0]);
isLocalPart[0] <== isPlusAndAt[0];
for (var i = 1; i < maxLength; i++) {
isPlus[i] <== IsEqual()([encoded[i], 43]);
isAt[i] <== IsEqual()([encoded[i], 64]);
isPlusAndAt[i] <== (1 - isPlus[i]) * (1 - isAt[i]);
isLocalPart[i] <== isLocalPart[i-1] * isPlusAndAt[i];
}
// Check for '.' (46 in ASCII)
for (var i = 0; i < maxLength; i++) {
isPeriod[i] <== IsEqual()([encoded[i], 46]);
isLocalPeriod[i] <== isLocalPart[i] * isPeriod[i];
}
// Track if we've found a plus and are in alias part
foundPlus[0] <== isPlus[0];
for (var i = 1; i < maxLength; i++) {
foundPlus[i] <== foundPlus[i-1] + isPlus[i];
}
// Determine if we're in the alias part (between + and @)
afterPlus[0] <== (1 - isPlus[0]);
for (var i = 1; i < maxLength; i++) {
afterPlus[i] <== afterPlus[i-1] * (1 - isPlus[i]);
}
hasAliasPart <== (1 - afterPlus[maxLength - 1]);
afterAt[0] <== (1 - isAt[0]);
for (var i = 1; i < maxLength; i++) {
afterAt[i] <== afterAt[i-1] * (1 - isAt[i]);
}
for (var i = 0; i < maxLength; i++) {
inAliasPartTemp[i] <== afterAt[i] - afterPlus[i];
inAliasPart[i] <== hasAliasPart * inAliasPartTemp[i];
}
// Determine which characters should be removed
for (var i = 0; i < maxLength; i++) {
shouldRemove[i] <== isLocalPeriod[i] + inAliasPart[i];
}
// Process the encoded input
for (var i = 0; i < maxLength; i++) {
processed[i] <== (1 - shouldRemove[i]) * encoded[i];
}
// Calculate powers of r for encoded (skip removed chars)
muxEnc[0] = Mux1();
muxEnc[0].c[0] <== r;
muxEnc[0].c[1] <== 1;
muxEnc[0].s <== shouldRemove[0];
rEnc[0] <== muxEnc[0].out;
for (var i = 1; i < maxLength; i++) {
muxEnc[i] = Mux1();
muxEnc[i].c[0] <== rEnc[i - 1] * r;
muxEnc[i].c[1] <== rEnc[i - 1];
muxEnc[i].s <== shouldRemove[i];
rEnc[i] <== muxEnc[i].out;
}
// Calculate powers of r for decoded
rDec[0] <== r;
for (var i = 1; i < maxLength; i++) {
rDec[i] <== rDec[i - 1] * r;
}
// Calculate running sum for processed
sumEnc[0] <== rEnc[0] * processed[0];
for (var i = 1; i < maxLength; i++) {
sumEnc[i] <== sumEnc[i - 1] + rEnc[i] * processed[i];
}
// Calculate running sum for decoded
sumDec[0] <== rDec[0] * decoded[0];
for (var i = 1; i < maxLength; i++) {
sumDec[i] <== sumDec[i - 1] + rDec[i] * decoded[i];
}
// Verify that processed matches decoded
isValid <== IsEqual()([sumEnc[maxLength - 1], sumDec[maxLength - 1]]);
}