ERC-7812 ZK identity registry
Review · Signatures & auth · created 2024-11-08
Commitments to secret data are stored in a single on-chain registry at a fixed address, so that later, without disclosing the data, their validity and authenticity can be proven via zero-knowledge proofs.
Why it is needed
Many privacy protocols use zero-knowledge proofs, but where and how a provable statement is stored has varied widely. When each protocol builds its own storage, a statement created in one place is hard for another protocol to reuse, and moving it across chains is cumbersome. The source states that this proposal arose from the need to consolidate such storage and issuance in one place. Gathering provable statements into a single standardized registry greatly improves reusability, portability, and security, and lets future protocols connect to this registry to make use of each other's data. The registry is also deliberately defined abstractly, so identity-based, reputation-based, proof-of-attendance-based, and other protocols can be implemented with few constraints. The proposal lays only the foundation, leaving concrete statement and commitment structures to be defined in separate EIPs.
What to implement
event RootUpdated(bytes32 indexed prev, bytes32 indexed curr);: An event that must fire whenever EvidenceDB's Merkle root changes. It carries the previous root and the new root.function addStatement(bytes32 key, bytes32 value) external;: Adds a new statement to the database. If the isolation key, made by hashing the caller's address with the key, already exists, it reverts.function removeStatement(bytes32 key) external;: Removes a statement from the database. It reverts if the isolation key does not exist.function updateStatement(bytes32 key, bytes32 newValue) external;: Replaces an existing statement's value with a new value. It reverts if the isolation key does not exist.function getRootTimestamp(bytes32 root) external view returns (uint256);: Looks up the timestamp at which a past database root was created. Returns block.timestamp for the latest root, or 0 if the root does not exist.function getIsolatedKey(address source, bytes32 key) external view returns (bytes32);: Hashes the calling address with the key to produce and return an isolation key exclusive to that caller.function add(bytes32 key, bytes32 value) external;: Adds a new element to the EvidenceDB tree. Only EvidenceRegistry can call this.function remove(bytes32 key) external;: Removes an element from the EvidenceDB tree.function update(bytes32 key, bytes32 newValue) external;: Updates the value of an element in the EvidenceDB tree.function getRoot() external view returns (bytes32);: Returns the SMT root. Should not be used on-chain due to the risk of root front-running.function getSize() external view returns (uint256);: Returns the number of nodes contained in the tree.function getMaxHeight() external view returns (uint256);: Returns the maximum height of the tree, i.e., the number of branches in a Merkle proof.function getProof(bytes32 key) external view returns (Proof memory);: Returns a Merkle inclusion or exclusion proof for the given key.function getValue(bytes32 key) external view returns (bytes32);: Looks up an element's value by key.
In depth
Three components: EvidenceRegistry, EvidenceDB, Registrar
This system consists of EvidenceRegistry, EvidenceDB, and Registrar. EvidenceRegistry is the entry point to EvidenceDB, a provable database shared by the entire protocol, and it can record arbitrary 32 byte data and prove it later. Registrar is the entity that implements concrete business use cases such as identity, reputation, or proof of attendance, structuring data and inserting it into EvidenceDB through EvidenceRegistry. This EIP standardizes only the first two components and treats the Registrar specification as a separate proposal. Inclusion or exclusion of data is proven with Merkle proofs, and knowing how a Registrar inserts data lets anyone implement an on chain ZK verifier using any stack, such as Circom.
Requirements for EvidenceDB
EvidenceDB can use any provable key/value data structure, but it must support addition, update, and removal of elements. All write operations must preserve idempotence, and adding then removing an element must leave the database state unchanged. The chosen data structure must be able to provide both inclusion and exclusion proofs for elements, and only EvidenceRegistry may call the state-changing functions. The reference interface is based on a sparse Merkle tree and defines a Proof struct that holds the root, an array of sibling hashes, an existence flag, and auxiliary node information (auxKey, auxValue, auxExistence).
Per-caller key isolation
addStatement, removeStatement, and updateStatement must isolate statement keys, assigning each caller a dedicated namespace. The isolation scheme is isolatedKey = hash(msg.sender, key), and the call must revert if the isolated key to be added already exists, or if the isolated key to be removed or updated does not exist. This namespace separation is designed to partition write access to database cells so that no one but the issuer can change their contents. However, this decision places access control management entirely on the Registrar, which is an important consideration when developing a Registrar.
Root history management and getRootTimestamp
EvidenceRegistry must maintain a linear history of EvidenceDB roots. getRootTimestamp must never revert, and must return 0 if the queried root does not exist, or block.timestamp if the latest root is queried. On chain, only minimal root history is kept to limit gas cost, and if more detailed history is needed, it is recommended to implement an off chain service that parses the RootUpdated event.
Hash function and prime field constraints
EvidenceRegistry and EvidenceDB must use the same secure hash function, and a ZK friendly hash function such as Poseidon is recommended to make database proofs easier. When using a ZK friendly hash, EvidenceRegistry must not accept keys or values that exceed the prime field size of the underlying elliptic curve (21888242871839275222246405745257275088548364400416034343698204186575808495617 for BN128). The reference implementation uses the Poseidon hash and includes an onlyInPrimeField check that enforces this range.
Singleton design and deterministic deployment
During specification, a per-protocol registry approach was compared against a shared singleton registry used by all protocols, and the latter was chosen. The rationale rests on three points: cross chain portability, since proving registry state across chains only requires sending a single bytes32 SMT root; concentrated trust, since users only need to trust one permissionless, immutable contract; and simplified integration, since formalizing the interface, hash function, and proof structure makes integration straightforward. EvidenceRegistry is deployed at 0x781246D2256dc0C1d8357c9dDc1eEe926a9c7812 using the deterministic deployment proxy at 0x4e59b44847b379578588920ca78fbf26c0b4956c with salt 0x04834e077c463de76a20df3770a7b96a5e5eb826922d1514f943cd5b41ccaed0.
Reference implementation and follow-up standards
The reference implementation provides Solidity contracts for EvidenceRegistry and EvidenceDB, plus a Circom circuit that verifies registry state. The SMT height is set to 80, and the circuit takes the root as a public input, recomputes the isolated key from the address and key, and checks the inclusion or exclusion proof. The implementation depends on @openzeppelin/contracts v5.1.0 and circomlib v2.0.5. Building on this proposal, follow-up EIPs are expected that specify commitment generation, commitment key management, and statement proof procedures for registrars such as one for recording national passports on chain, one for EIP-4337 confidential account identity management, and one for POAP.
Proposal status: Review
This proposal is still at the Review stage. Since the content may change, check the latest source before implementing.
Things to watch out for
- Trusted setup risk in ZK verifiers: Individual registrars are expected to provide both management of statements and proof functionality, and proofs are usually zero knowledge proofs that require a trusted setup. A ZK verifier with a flawed trusted setup can be abused to pass forged proofs.
- getRoot must not be used on chain: An integrating registrar must not use EvidenceDB's getRoot on chain to check database state, because the root can be changed through front running. Instead, the needed root should be passed in as a function argument and checked with getRootTimestamp.