Skip to content

Latest commit

 

History

History
130 lines (76 loc) · 9.72 KB

README.md

File metadata and controls

130 lines (76 loc) · 9.72 KB

Semaphore Modular Smart Account Modules

Overview

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

Using the Module

# Install dependencies
pnpm install

# Build the project
pnpm run build

# Run unit tests and integration tests
pnpm run test

Developer Documentation

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.

Smart Contract Storage

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 the smaphore 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 the ExtCallCount 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.

API

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.

  1. 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 the msg.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)).

  2. 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 from initiatedTx() previously, to specify the proving transaction.
    • proof: The zero-knowledge Semaphore proof that the transaction txHash 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().

Signature and Calldata

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.

UserOp Signature

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:

UserOp Signature

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.

calldata-packedUserOp

Verifying EdDSA Signature

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.

ERC-1271 and ERC-7780

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().

Testing

The testing code relies on Foundry FFI to call Semaphore typescript API to generate zero-knowledge proof and EdDSA signature.

Relevant Information

ERC-4337 Lifecycle on Validation

ERC-4337 Lifecycle

Source: ERC-4337 website

Contributions

Thanks to the following folks on discussing about this project and helps along: