Skip to content

Commit f27f1a1

Browse files
committed
Add AccountZKEmail.test.js
1 parent 799cf6c commit f27f1a1

File tree

3 files changed

+174
-1
lines changed

3 files changed

+174
-1
lines changed

contracts/utils/cryptography/SignerZKEmail.sol

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ abstract contract SignerZKEmail is AbstractSigner {
6363
*
6464
* The account salt is used for:
6565
*
66-
* * Privacy: Enables email address privacy on-chain so long as the randomly generated account code is not revealed to an adversary.
66+
* * Privacy: Enables email address privacy on-chain so long as the randomly generated account code is not revealed
67+
* to an adversary.
6768
* * Security: Provides a unique identifier that cannot be easily guessed or brute-forced, as it's derived
6869
* from both the email address and a random account code.
6970
* * Deterministic Address Generation: Enables the creation of deterministic addresses based on email addresses,
@@ -124,6 +125,25 @@ abstract contract SignerZKEmail is AbstractSigner {
124125
bytes32 hash,
125126
bytes calldata signature
126127
) internal view virtual override returns (bool) {
128+
// Check if the signature is long enough to contain the EmailAuthMsg
129+
// The minimum length is 512 bytes (initial part + pointer offsets)
130+
// - `templateId` is a uint256 (32 bytes).
131+
// - `commandParams` is a dynamic array of bytes32 (32 bytes offset).
132+
// - `skippedCommandPrefixSize` is a uint256 (32 bytes).
133+
// - `proof` is a struct with the following fields (32 bytes offset):
134+
// - `domainName` is a dynamic string (32 bytes offset).
135+
// - `publicKeyHash` is a bytes32 (32 bytes).
136+
// - `timestamp` is a uint256 (32 bytes).
137+
// - `maskedCommand` is a dynamic string (32 bytes offset).
138+
// - `emailNullifier` is a bytes32 (32 bytes).
139+
// - `accountSalt` is a bytes32 (32 bytes).
140+
// - `isCodeExist` is a boolean, so its length is 1 byte padded to 32 bytes.
141+
// - `proof` is a dynamic bytes (32 bytes offset).
142+
// There are 128 bytes for the EmailAuthMsg type and 256 bytes for the proof.
143+
// Considering all dynamic elements are empty (i.e. `commandParams` = [], `domainName` = "", `maskedCommand` = "", `proof` = []),
144+
// then we have 128 bytes for the EmailAuthMsg type, 256 bytes for the proof and 4 * 32 for the lenght of the dynamic elements.
145+
// So the minimum length is 128 + 256 + 4 * 32 = 512 bytes.
146+
if (signature.length < 512) return false;
127147
EmailAuthMsg memory emailAuthMsg = abi.decode(signature, (EmailAuthMsg));
128148
return (abi.decode(emailAuthMsg.commandParams[0], (bytes32)) == hash &&
129149
emailAuthMsg.templateId == templateId() &&

test/account/AccountZKEmail.test.js

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
const { ethers } = require('hardhat');
2+
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');
3+
4+
const { ERC4337Helper } = require('../helpers/erc4337');
5+
6+
const { shouldBehaveLikeAccountCore, shouldBehaveLikeAccountHolder } = require('./Account.behavior');
7+
const { shouldBehaveLikeERC1271 } = require('../utils/cryptography/ERC1271.behavior');
8+
const { shouldBehaveLikeERC7821 } = require('./extensions/ERC7821.behavior');
9+
const { ZKEmailSigningKey, NonNativeSigner } = require('../helpers/signers');
10+
11+
const accountSalt = '0x046582bce36cdd0a8953b9d40b8f20d58302bacf3bcecffeb6741c98a52725e2'; // keccak256("[email protected]")
12+
const selector = '12345';
13+
const domainName = 'gmail.com';
14+
const publicKeyHash = '0x0ea9c777dc7110e5a9e89b13f0cfc540e3845ba120b2b6dc24024d61488d4788';
15+
const emailNullifier = '0x00a83fce3d4b1c9ef0f600644c1ecc6c8115b57b1596e0e3295e2c5105fbfd8a';
16+
const templateId = ethers.solidityPackedKeccak256(['string', 'uint256'], ['TEST', 0n]);
17+
18+
const SIGN_HASH_COMMAND = 'signHash';
19+
20+
async function fixture() {
21+
// EOAs and environment
22+
const [admin, beneficiary, other] = await ethers.getSigners();
23+
const target = await ethers.deployContract('CallReceiverMockExtended');
24+
25+
// Registry
26+
const dkim = await ethers.deployContract('ECDSAOwnedDKIMRegistry');
27+
await dkim.initialize(admin, admin);
28+
await dkim
29+
.SET_PREFIX()
30+
.then(prefix => dkim.computeSignedMsg(prefix, domainName, publicKeyHash))
31+
.then(message => admin.signMessage(message))
32+
.then(signature => dkim.setDKIMPublicKeyHash(selector, domainName, publicKeyHash, signature));
33+
34+
// Verifier
35+
const verifier = await ethers.deployContract('ZKEmailVerifierMock');
36+
37+
// ERC-4337 signer
38+
const signer = new NonNativeSigner(
39+
new ZKEmailSigningKey(domainName, publicKeyHash, emailNullifier, accountSalt, templateId),
40+
);
41+
42+
// ERC-4337 account
43+
const helper = new ERC4337Helper();
44+
const mock = await helper.newAccount('$AccountZKEmailMock', [accountSalt, dkim.target, verifier.target, templateId]);
45+
46+
const signUserOp = async userOp => {
47+
// Create email auth message for the user operation hash
48+
const hash = await userOp.hash();
49+
return Object.assign(userOp, { signature: signer.signingKey.sign(hash).serialized });
50+
};
51+
52+
const userOpInvalidSig = async userOp => {
53+
// Create email auth message for the user operation hash
54+
const hash = await userOp.hash();
55+
const timestamp = Math.floor(Date.now() / 1000);
56+
const command = SIGN_HASH_COMMAND + ' ' + ethers.toBigInt(hash).toString();
57+
const isCodeExist = true;
58+
const proof = '0x00'; // Mocked in ZKEmailVerifierMock
59+
60+
// Encode the email auth message as the signature
61+
return ethers.AbiCoder.defaultAbiCoder().encode(
62+
['tuple(uint256,bytes[],uint256,tuple(string,bytes32,uint256,string,bytes32,bytes32,bool,bytes))'],
63+
[
64+
[
65+
templateId,
66+
[hash],
67+
0, // skippedCommandPrefix
68+
[domainName, publicKeyHash, timestamp, command, emailNullifier, accountSalt, isCodeExist, proof],
69+
],
70+
],
71+
);
72+
};
73+
74+
return { helper, mock, dkim, verifier, target, beneficiary, other, signUserOp, userOpInvalidSig, signer };
75+
}
76+
77+
describe('AccountZKEmail', function () {
78+
beforeEach(async function () {
79+
Object.assign(this, await loadFixture(fixture));
80+
});
81+
82+
shouldBehaveLikeAccountCore();
83+
shouldBehaveLikeAccountHolder();
84+
shouldBehaveLikeERC1271({ erc7739: true });
85+
shouldBehaveLikeERC7821();
86+
});

test/helpers/signers.js

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const {
2+
AbiCoder,
23
AbstractSigner,
34
Signature,
45
TypedDataEncoder,
@@ -13,6 +14,7 @@ const {
1314
hexlify,
1415
sha256,
1516
toBeHex,
17+
toBigInt,
1618
} = require('ethers');
1719
const { secp256r1 } = require('@noble/curves/p256');
1820
const { generateKeyPairSync, privateEncrypt } = require('crypto');
@@ -156,9 +158,74 @@ class RSASHA256SigningKey extends RSASigningKey {
156158
}
157159
}
158160

161+
class ZKEmailSigningKey {
162+
#domainName;
163+
#publicKeyHash;
164+
#emailNullifier;
165+
#accountSalt;
166+
#templateId;
167+
168+
constructor(domainName, publicKeyHash, emailNullifier, accountSalt, templateId) {
169+
this.#domainName = domainName;
170+
this.#publicKeyHash = publicKeyHash;
171+
this.#emailNullifier = emailNullifier;
172+
this.#accountSalt = accountSalt;
173+
this.#templateId = templateId;
174+
this.SIGN_HASH_COMMAND = 'signHash';
175+
}
176+
177+
get domainName() {
178+
return this.#domainName;
179+
}
180+
181+
get publicKeyHash() {
182+
return this.#publicKeyHash;
183+
}
184+
185+
get emailNullifier() {
186+
return this.#emailNullifier;
187+
}
188+
189+
get accountSalt() {
190+
return this.#accountSalt;
191+
}
192+
193+
sign(digest /*: BytesLike*/ /*: Signature*/) {
194+
const timestamp = Math.floor(Date.now() / 1000);
195+
const command = this.SIGN_HASH_COMMAND + ' ' + toBigInt(digest).toString();
196+
const isCodeExist = true;
197+
const proof = '0x01'; // Mocked in ZKEmailVerifierMock
198+
199+
// Encode the email auth message as the signature
200+
return {
201+
serialized: AbiCoder.defaultAbiCoder().encode(
202+
['tuple(uint256,bytes[],uint256,tuple(string,bytes32,uint256,string,bytes32,bytes32,bool,bytes))'],
203+
[
204+
[
205+
this.#templateId,
206+
[digest],
207+
0, // skippedCommandPrefix
208+
[
209+
this.#domainName,
210+
this.#publicKeyHash,
211+
timestamp,
212+
command,
213+
this.#emailNullifier,
214+
this.#accountSalt,
215+
isCodeExist,
216+
proof,
217+
],
218+
],
219+
],
220+
),
221+
};
222+
}
223+
}
224+
159225
module.exports = {
160226
NonNativeSigner,
161227
P256SigningKey,
162228
RSASigningKey,
163229
RSASHA256SigningKey,
230+
ZKEmailSigningKey,
164231
};

0 commit comments

Comments
 (0)