This project is a validator and executor module adheres to ERC-7579 standard that uses Semaphore for proof validation. Smart accounts incorporate this validator gains the following benefits:
-
The smart account behaves like a M-of-N multi-sig wallet controlled by members of the Semaphore group of the smart account. Proofs sent by the members are used as signatures.
-
The smart accout gains Semaphore property members preserve their privacy that no one know who send the proof (signature) except they must belong to the group while guaranteeing they have not signed before.
Development of this project is supported by PSE Acceleration Program (see thread discussion).
Project Code: FY24-1847
# Install dependencies
pnpm install
# Build the project
pnpm run build
# Run unit tests and integration tests
pnpm run test
There are two ERC-7579 modules in this repo, namely SemaphoreValidator and SemaphoreExecutor.
SemaphoreValidator is responsible for validating the signature of the UserOp is indeed a valid EdDSA signature of the UserOp hash from the public key included in the signature. The validator module checks the commitment of the public key is indeed a member of the Sempahore group of the smart account. Note this module also restricts the smart account to be able to call SempahoreExecutor contract and its three APIs only.
SempahoreExecutor has all the account states stored and provides three key APIs: initiateTx(), signTx(), and executeTx(). initiateTx() is responsible for Semaphore members of the smart account to initiate an external transaction. signTx() is for collecting enough proofs. The proofs could be seen as a "signature" from the members who approve the tx. Lastly, executeTx() is to actually trigger the execution of the transaction.
Because SemaphoreExecutor is an executor module, the called contract will see the smart account as the msg.sender
, not the executor contract.
There is a one-time set function setSemaphoreValidator() in the executor to set the associated validator. This is to ensure that a smart account uninstall the validator first before the executor. Otherwise it will render the smart account unusable.
SemaphoreExecutor contract stores the following information on-chain.
groupMapping
: This object maps from the smart account address to a Semaphore group.thresholds
: The threshold number of proofs a particular smart account needs to collect for a transaction to be executed.memberCount
: The member count of a Semaphore group. The actual member commitments are stored in thesmaphore
contract Lean Incremental Merkle Tree structure.acctTxCount
: This object stores the transaction call data and value that are waiting to be proved (signed), and the proofs it has collected so far. This information is stored in theExtCallCount
data structure.acctSeqNum
: The sequence number corresponding to a smart account. This value is used when generating a transaction signature to uniquely identify a particular transaction.
After installing the two modules, the smart account can only call three functions in the executor module, initiateTx(), signTx(), and executeTx(). Calling other functions would be rejected in the validateUserOp() check.
-
initiateTx(): for the Semaphore member to initate a new transaction of the Smart account. This function checks the validity of the semaphore proof and corresponding parameters. It takes five paramters.
target
: The target address of the transaction.value
: any balance to be used. It will be used as themsg.value
in the actual external transaction.callData
: The call data to the target address. The first four bytes are the target function selector, and the rest function payload. For EOA value transfer, this value should be null (zero-length byte).proof
: The zero-knowledge Semaphore proof generated off-chain to prove a member signs the transaction.execute
: Boolean value to indicate if the transaction reaches the proof collection threshold, whether to execute the transaction immediately.
An ExtCallCount object is created to store the user transaction call data.
A 32-byte hash txHash is returned, generated from
keccak256(abi.encodePacked(seq, targetAddr, value, txCallData))
. -
signTx(): for other Semaphore member to sign a previously initiated transaction. Again, it checks the Semaphore proof, if the hash and the proof are valid, the proof count is incremented.
txHash
: The hash value returned frominitiatedTx()
previously, to specify the proving transaction.proof
: The zero-knowledge Semaphore proof that the transactiontxHash
corresponding to.execute
: Same as initiateTx().
3.executeTx(): call to execute the transaction specified by txHash
. If the transaction hasn't collected enough proofs, it would revert.
txHash
: Same as initiateTx().
Transactions from ERC-4337 will go through validateUserOp() for validation, based on userOp, and userOpHash. In validation, the key logic is to check three objects: the userOp hash (userOpHash
), the signature (signature
), and the target call data (targetCallData
).
A proper userOp signature is a 160 bytes value signed by EdDSA signature scheme. The signature itself is 32 * 3 = 96 bytes, but we also prepend the identity public key uses for validation.
The userOpHash
is 32-byte long, it is a keccak256() of sequence number, target address, value, and the target parameters.
For the UserOp calldata passing to getExecOps()
in testing, it is:
Now, when decoding the calldata from PackedUserOperation object in validateUserOp(), the above calldata is combined with other information and what we are interested started from the 100th byte, as shown below.
A Semaphore identity consists of an EdDSA public/private key pair and a commitment. Semaphore uses an EdDSA implementation based on Baby Jubjub and Poseidon. The actual implementation is in zk-kit repository.
We implement the identity verification logic Identity.verifySignature() on-chain. We also have a Identity.verifySignatureFFI() function for testing to compare the result with calling Semaphore typescript-based implementation. It relies on the Baby JubJub curve Solidity implementataion by yondonfu with a minor fix.
The module is also compatible with:
- ERC-1271: Accepting signature from other smart contract by implementing
isValidSignatureWithSender()
. - ERC-7780: Being a Stateless Validator by implementing
validateSignatureWithData()
.
The testing code relies on Foundry FFI to call Semaphore typescript API to generate zero-knowledge proof and EdDSA signature.
Source: ERC-4337 website
Thanks to the following folks on discussing about this project and helps along:
- Saleel P on initiating this idea with Semaphore Wallet, showing me that the idea is feasible.
- Cedoor and Vivian Plasencia on Semaphore development and their opinions.
- John Guilding on the discussion, support, and review of the project.
- Konrad Kopp on the support of using ModuleKit framework which this module is built upon, and answering my question on some details of ERC-4337 standard.