diff --git a/ERCS/erc-7812.md b/ERCS/erc-7812.md new file mode 100644 index 0000000000..cd74096ca3 --- /dev/null +++ b/ERCS/erc-7812.md @@ -0,0 +1,522 @@ +--- +eip: 7812 +title: ZK Identity Registry +description: Singleton registry system for storing abstract private provable statements. +author: Artem Chystiakov (@arvolear) , Oleksandr Kurbatov , Yaroslav Panasenko , Michael Elliot (@michaelelliot) , Vitalik Buterin (@vbuterin) +discussions-to: https://ethereum-magicians.org/t/erc-7812-zk-identity-registry/21624 +status: Draft +type: Standards Track +category: ERC +created: 2024-11-08 +--- + +## Abstract + +This EIP introduces an on-chain registry system for storing and proving abstract statements. Users may utilize the system to store commitments to their private data to later prove its validity and authenticity via zero knowledge, without disclosing anything about the data itself. Moreover, developers may use the singleton `EvidenceRegistry` contract available at `0x` to integrate custom business-specific registrars for managing and processing particular statements. + +## Motivation + +This EIP stemmed from the need to localize and unravel the storage and issuance of provable statements so that future protocols can anchor to the standardized singleton on-chain registry and benefit from cross-reuse. + +The aggregation of provable statements significantly improves reusability, portability, and security of the abundance of zero knowledge privacy-oriented solutions. The abstract specification of the registry allows custom indentity-based, reputation-based, proof-of-attendance-based, etc., protocols to be implemented with little to minimal constraints. + +The given proposal lays the important foundation for specific solution to build upon. The more concrete specifications of statements and commitments structures are expected to emerge as separate, standalone EIPs. + +## Specification + +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119. + +### Definitions + +- A "Sparse Merkle Tree (SMT)" is a special Merkle tree that works by deterministically and idempotently storing key/value pairs in the given locations leveraging a hash function. The Poseidon hash function is often used to optimize the compatibility with ZK. +- A "statement" is an accepted structured representation of some abstract evidence. A statement can range from a simple `string` to a Merkle root of some SMT. +- A "commitment" is a special public value resulting from blinding a statement to conceal it. Commitments allow the authenticity of a statement to be proven in ZK without disclosing the statement itself. +- A "commitment key" is a private salt mixed with the statement to obtain a commitment to that statement. The commitment key must be kept private to maintain the confidentiality of statements. + +### General + +The on-chain registry system consists of two subsystems: the `EvidenceRegistry` with `EvidenceDB` and `Registrar` components. This EIP will focus on describing and standardizing the former, while the `Registrar` specification may be amended as the separate proposals. + +![The on-chain evidence registry system entities diagram.](../assets/eip-7812/images/diagram.png) + +The on-chain evidence registry system entities diagram. + +The `EvidenceRegistry` acts as the entrypoint to a protocol-wide provable database `EvidenceDB` where arbitrary `32-byte` data can be written to and later proven on demand. The `Registrar` entities implement specific business use cases, structure the provable data, and utilize `EvidenceRegistry` to put this data in the `EvidenceDB`. + +In order to prove that certain data is or is not present in the `EvidenceDB` Merkle proofs may be used. Understanding how a specific `Registrar` has structured and put data into the `EvidenceDB`, one may implement an on-chain ZK verifier (using Circom or any other stack) and prove the inclusion (or exclusion) of the data in the database. + +The Circom implementation of a general-purpose SMT-driven `EvidenceDB` verifier circuit together with the Solidity implementation of `EvidenceRegistry` and `EvidenceDB` smart contracts may be found in the "Reference Implementation" section. + +### Evidence DB + +The `EvidenceDB` smart contract MAY implement an arbitrary provable key/value data structure, however it MUST support the `addition`, `update`, and `removal` of elements. All of the supported write operations MUST maintain the property of idempotence (e.i. `addition` followed by `removal` should not change the state of the database). The data structure of choice MUST be capable of providing both element inclusion and exclusion proofs. The functions that modify the `EvidenceDB` state MUST be callable only by the `EvidenceRegistry`. + +For reference, the `EvidenceDB` smart contract MAY implement the following interface: + +```solidity +pragma solidity ^0.8.0; + +/** + * @notice Evidence DB interface for Sparse Merkle Tree based statements database. + */ +interface IEvidenceDB { + /** + * @notice Represents the proof of a node's inclusion/exclusion in the tree. + * @param root The root hash of the Merkle tree. + * @param siblings An array of sibling hashes can be used to get the Merkle Root. + * @param existence Indicates the presence (true) or absence (false) of the node. + * @param key The key associated with the node. + * @param value The value associated with the node. + * @param auxExistence Indicates the presence (true) or absence (false) of an auxiliary node. + * @param auxKey The key of the auxiliary node. + * @param auxValue The value of the auxiliary node. + */ + struct Proof { + bytes32 root; + bytes32[] siblings; + bool existence; + bytes32 key; + bytes32 value; + bool auxExistence; + bytes32 auxKey; + bytes32 auxValue; + } + + /** + * @notice Adds the new element to the tree. + */ + function add(bytes32 key, bytes32 value) external; + + /** + * @notice Removes the element from the tree. + */ + function remove(bytes32 key) external; + + /** + * @notice Updates the element in the tree. + */ + function update(bytes32 key, bytes32 newValue) external; + + /** + * @notice Gets the SMT root. + * SHOULD NOT be used on-chain due to roots frontrunning. + */ + function getRoot() external view returns (bytes32); + + /** + * @notice Gets the number of nodes in the tree. + */ + function getSize() external view returns (uint256); + + /** + * @notice Gets the max tree height (number of branches in the Merkle proof) + */ + function getMaxHeight() external view returns (uint256); + + /** + * @notice Gets Merkle inclusion/exclusion proof of the element. + */ + function getProof(bytes32 key) external view returns (Proof memory); + + /** + * @notice Gets the element value by its key. + */ + function getValue(bytes32 key) external view returns (bytes32); +} +``` + +### Evidence Registry + +The `EvidenceRegistry` smart contract is the central piece of this EIP. The `EvidenceRegistry` MUST implement the following interface, however, it MAY be extended: + +```solidity +pragma solidity ^0.8.0; + +/** + * @notice Common Evidence Registry interface. + */ +interface IEvidenceRegistry { + /** + * @notice MUST be emitted whenever the Merkle root is updated. + */ + event RootUpdated(bytes32 indexed prev, bytes32 indexed curr); + + /** + * @notice Adds the new statement to the DB. + */ + function addStatement(bytes32 key, bytes32 value) external; + + /** + * @notice Removes the statement from the DB. + */ + function removeStatement(bytes32 key) external; + + /** + * @notice Updates the statement in the DB. + */ + function updateStatement(bytes32 key, bytes32 newValue) external; + + /** + * @notice Retrieves historical DB roots creation timestamps. + * Latest root MUST return `block.timestamp`. + * Non-existent root MUST return `0`. + */ + function getRootTimestamp(bytes32 root) external view returns (uint256); +} +``` + +The `addStatement`, `removeStatement`, and `updateStatement` methods MUST isolate the statement `key` in order for the database to allocate a specific namespace for a caller. These methods MUST revert in case the isolated key being added already exists in the `EvidenceDB` or the isolated key being removed or updated does not. + +The `EvidenceRegistry` MUST maintain the linear history of `EvidenceDB` roots. The `getRootTimestamp` method MUST NOT revert. Instead, it MUST return `0` in case the queried `root` does not exist. The method MUST return `block.timestamp` in case the latest root is requested. + +Before communicating with the `EvidenceDB`, the `key` MUST be isolated in the following way: + +```solidity +bytes32 isolatedKey = hash(msg.sender, key) +``` + +Where the `hash` is secure protocol-wide hash function of choice. + +### Hash Function + +The same secure hash function MUST be employed in both `EvidenceRegistry` and `EvidenceDB`. It is RECOMMENDED to use ZK-friendly hash function such as `poseidon` to streamline the database proving. + +In case ZK-friendly hash function is chosen, `EvidenceRegistry` MUST NOT accept `keys` or `values` beyond the underlying elliptic curve prime field size (`21888242871839275222246405745257275088548364400416034343698204186575808495617` for `BN128`). + +## Rationale + +During the EIP specification we have considered two approaches: where every protocol has its own registry and where all protocols are united under a singleton registry. We have decided to go with the latter as this approach provides the following benefits: + +1. Cross-chain portability. Only a single `bytes32` value (the SMT root) has to be sent cross-chain to be able to prove the state of the registry. +2. Centralization of trust. Users only need to trust a single, permissionaless, immutable smart contract. +3. Integration streamline. The singleton design formalizes the system interface, the hash function, and the overall proofs structure to simplify the integration. + +The proposal is deliberately written as abstract as possible to not constrain the possible business use cases and allow `Registrars` to implement arbitrary provable solutions. + +It is expected that based on this work future EIPs will describe concrete registrars with the exact procedures of generation of commitments, management of commitment keys, and proving of operated statements. For instance, there may be a registrar for on-chain accounting of national passports, a registrar with [EIP-4337](./eip-4337.md) confidential account identity management, a registrar for POAPs, etc. + +The `EvidenceDB` namespacing is chosen to segregate the write access to the database cells, ensuring that no entity but issuer can alter their content. However, this decision delegates the access control management responsibility solely to registrars, an important aspect to be considered during their development. + +The `EvidenceRegistry` maintains the minimal viable (gas-wise) history of roots on-chain for smooth registrars integration. In case more elaborate history is required, it is RECOMMENDED to implement off-chain services for parsing of `RootUpdated` events. + +## Backwards Compatibility + +This EIP is fully backwards compatible. + +### Deployment Method + +The `EvidenceRegistry` is a singleton contract available at `0x` deployed via the "deterministic deployment proxy" from `0x4e59b44847b379578588920ca78fbf26c0b4956c` with the salt `0x` . + +## Reference Implementation + +The reference implementation of `EvidenceRegistry` and `EvidenceDB` Solidity smart contracts together with the evidence registry state verifier Circom circuit is provided in the proposal. + +The low-level Solidity and Circom implementations of SMT can be found [here](../assets/eip-7812/contracts/SparseMerkleTree.sol) and [here](../assets/eip-7812/circuits/SparseMerkleTree.circom). + +The height of the SMT is set to `80`. + +> Please note that the reference implementation depends on the `@openzeppelin/contracts v5.1.0` and `circomlib v2.0.5`. + +### EvidenceDB Implementation + +```solidity +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.21; + +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +import {IEvidenceDB} from "./interfaces/IEvidenceDB.sol"; + +import {SparseMerkleTree} from "./libraries/SparseMerkleTree.sol"; +import {PoseidonUnit2L, PoseidonUnit3L} from "./libraries/Poseidon.sol"; + +contract EvidenceDB is IEvidenceDB, Initializable { + using SparseMerkleTree for SparseMerkleTree.SMT; + + address private _evidenceRegistry; + + SparseMerkleTree.SMT private _tree; + + modifier onlyEvidenceRegistry() { + _requireEvidenceRegistry(); + _; + } + + function __EvidenceDB_init(address evidenceRegistry_, uint32 maxDepth_) external initializer { + _evidenceRegistry = evidenceRegistry_; + + _tree.initialize(maxDepth_); + + _tree.setHashers(_hash2, _hash3); + } + + /** + * @inheritdoc IEvidenceDB + */ + function add(bytes32 key_, bytes32 value_) external onlyEvidenceRegistry { + _tree.add(key_, value_); + } + + /** + * @inheritdoc IEvidenceDB + */ + function remove(bytes32 key_) external onlyEvidenceRegistry { + _tree.remove(key_); + } + + /** + * @inheritdoc IEvidenceDB + */ + function update(bytes32 key_, bytes32 newValue_) external onlyEvidenceRegistry { + _tree.update(key_, newValue_); + } + + /** + * @inheritdoc IEvidenceDB + */ + function getRoot() external view returns (bytes32) { + return _tree.getRoot(); + } + + /** + * @inheritdoc IEvidenceDB + */ + function getSize() external view returns (uint256) { + return _tree.getNodesCount(); + } + + /** + * @inheritdoc IEvidenceDB + */ + function getMaxHeight() external view returns (uint256) { + return _tree.getMaxDepth(); + } + + /** + * @inheritdoc IEvidenceDB + */ + function getProof(bytes32 key_) external view returns (Proof memory) { + return _tree.getProof(key_); + } + + /** + * @inheritdoc IEvidenceDB + */ + function getValue(bytes32 key_) external view returns (bytes32) { + return _tree.getNodeByKey(key_).value; + } + + /** + * @notice Returns the address of the Evidence Registry. + */ + function getEvidenceRegistry() external view returns (address) { + return _evidenceRegistry; + } + + function _requireEvidenceRegistry() private view { + if (_evidenceRegistry != msg.sender) { + revert NotFromEvidenceRegistry(msg.sender); + } + } + + function _hash2(bytes32 element1_, bytes32 element2_) private pure returns (bytes32) { + return PoseidonUnit2L.poseidon([element1_, element2_]); + } + + function _hash3( + bytes32 element1_, + bytes32 element2_, + bytes32 element3_ + ) private pure returns (bytes32) { + return PoseidonUnit3L.poseidon([element1_, element2_, element3_]); + } +} +``` + +### EvidenceRegistry Implementation + +```solidity +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.21; + +import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol"; + +import {IEvidenceDB} from "./interfaces/IEvidenceDB.sol"; +import {IEvidenceRegistry} from "./interfaces/IEvidenceRegistry.sol"; + +import {PoseidonUnit2L} from "./libraries/Poseidon.sol"; + +contract EvidenceRegistry is IEvidenceRegistry, Initializable { + uint256 public constant BABY_JUB_JUB_PRIME_FIELD = + 21888242871839275222246405745257275088548364400416034343698204186575808495617; + + IEvidenceDB private _evidenceDB; + + mapping(bytes32 => uint256) private _rootTimestamps; + + modifier onlyInPrimeField(bytes32 key) { + _requireInPrimeField(key); + _; + } + + modifier onRootUpdate() { + bytes32 prevRoot_ = _evidenceDB.getRoot(); + _rootTimestamps[prevRoot_] = block.timestamp; + _; + emit RootUpdated(prevRoot_, _evidenceDB.getRoot()); + } + + function __EvidenceRegistry_init(address evidenceDB_) external initializer { + _evidenceDB = IEvidenceDB(evidenceDB_); + } + + /** + * @inheritdoc IEvidenceRegistry + */ + function addStatement( + bytes32 key_, + bytes32 value_ + ) external onlyInPrimeField(key_) onlyInPrimeField(value_) onRootUpdate { + bytes32 isolatedKey_ = _getIsolatedKey(key_); + + if (_evidenceDB.getValue(isolatedKey_) != bytes32(0)) { + revert KeyAlreadyExists(key_); + } + + _evidenceDB.add(isolatedKey_, value_); + } + + /** + * @inheritdoc IEvidenceRegistry + */ + function removeStatement(bytes32 key_) external onlyInPrimeField(key_) onRootUpdate { + bytes32 isolatedKey_ = _getIsolatedKey(key_); + + if (_evidenceDB.getValue(isolatedKey_) == bytes32(0)) { + revert KeyDoesNotExist(key_); + } + + _evidenceDB.remove(isolatedKey_); + } + + /** + * @inheritdoc IEvidenceRegistry + */ + function updateStatement( + bytes32 key_, + bytes32 newValue_ + ) external onlyInPrimeField(key_) onlyInPrimeField(newValue_) onRootUpdate { + bytes32 isolatedKey_ = _getIsolatedKey(key_); + + if (_evidenceDB.getValue(isolatedKey_) == bytes32(0)) { + revert KeyDoesNotExist(key_); + } + + _evidenceDB.update(isolatedKey_, newValue_); + } + + /** + * @inheritdoc IEvidenceRegistry + */ + function getRootTimestamp(bytes32 root_) external view returns (uint256) { + if (root_ == bytes32(0)) { + return 0; + } + + if (root_ == _evidenceDB.getRoot()) { + return block.timestamp; + } + + return _rootTimestamps[root_]; + } + + function getEvidenceDB() external view returns (address) { + return address(_evidenceDB); + } + + function _getIsolatedKey(bytes32 key_) internal view returns (bytes32) { + return PoseidonUnit2L.poseidon([bytes32(uint256(uint160(msg.sender))), key_]); + } + + function _requireInPrimeField(bytes32 key_) private pure { + if (uint256(key_) >= BABY_JUB_JUB_PRIME_FIELD) { + revert NumberNotInPrimeField(key_); + } + } +} +``` + +### EvidenceRegistry Verifier Implementation + +```solidity +// LICENSE: CC0-1.0 +pragma circom 2.1.9; + +include "SparseMerkleTree.circom"; + +template BuildIsolatedKey() { + signal output isolatedKey; + + signal input address; + signal input key; + + component hasher = Poseidon(2); + hasher.inputs[0] <== address; + hasher.inputs[1] <== key; + + hasher.out ==> isolatedKey; +} + +template EvidenceRegistrySMT(levels) { + // Public Inputs + signal input root; + + // Private Inputs + signal input address; + signal input key; + + signal input value; + + signal input siblings[levels]; + + signal input auxKey; + signal input auxValue; + signal input auxIsEmpty; + + signal input isExclusion; + + // Build isolated key + component isolatedKey = BuildIsolatedKey(); + isolatedKey.address <== address; + isolatedKey.key <== key; + + // Verify Sparse Merkle Tree Proof + component smtVerifier = SparseMerkleTree(levels); + smtVerifier.siblings <== siblings; + + smtVerifier.key <== isolatedKey.isolatedKey; + smtVerifier.value <== value; + + smtVerifier.auxKey <== auxKey; + smtVerifier.auxValue <== auxValue; + smtVerifier.auxIsEmpty <== auxIsEmpty; + + smtVerifier.isExclusion <== isExclusion; + + smtVerifier.root <== root; +} + +component main {public [root]} = EvidenceRegistrySMT(80); +``` + +## Security Considerations + +From security standpoint there are several important aspects that must be highlighted. + +The individual registrars are expected to provide the functionality for both management and proving of statements. The proving will often be carried out by ZK proofs, which require trusted setup. Improperly setup ZK verifiers can be exploited to verify forged proofs. + +The `getRoot` method of `EvidenceDB` SHOULD NOT be used on-chain by the integrating registrars to check the validity of the database state. Instead, the required `root` SHOULD be passed as a function parameter and checked via `getRootTimestamp` method to avoid being frontrun. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE.md). diff --git a/assets/erc-7812/circuits/SparseMerkleTree.circom b/assets/erc-7812/circuits/SparseMerkleTree.circom new file mode 100644 index 0000000000..22231aa0b7 --- /dev/null +++ b/assets/erc-7812/circuits/SparseMerkleTree.circom @@ -0,0 +1,272 @@ +// LICENSE: MIT +pragma circom 2.1.9; + +include "circomlib/circuits/poseidon.circom"; +include "circomlib/circuits/switcher.circom"; +include "circomlib/circuits/gates.circom"; +include "circomlib/circuits/bitify.circom"; + +function inverse(a) { + return 1 - a; +} + +/* + * Hash2 = Poseidon(H_L | H_R) + */ +template Hash2() { + signal input a; + signal input b; + + signal output out; + + component h = Poseidon(2); + h.inputs[0] <== a; + h.inputs[1] <== b; + + out <== h.out; +} + +/* + * Hash2 = Poseidon(key | value | 1) + * 1 is added to the end of the leaf value to make the hash unique + */ +template Hash3() { + signal input a; + signal input b; + signal input c; + + signal output out; + + c === 1; + + component h = Poseidon(3); + h.inputs[0] <== a; + h.inputs[1] <== b; + h.inputs[2] <== c; + + out <== h.out; +} + +/* +* Returns an array of bits, where the index of `1` bit +* is the current depth of the tree +*/ +template DepthDeterminer(depth) { + assert(depth > 1); + + signal input siblings[depth]; + signal output desiredDepth[depth]; + + signal done[depth - 1]; + + component isZero[depth]; + + for (var i = 0; i < depth; i++) { + isZero[i] = IsZero(); + isZero[i].in <== siblings[i]; + } + + // The last sibling is always zero due to the way the proof is constructed + isZero[depth - 1].out === 1; + + // If there is a branch on the previous depth, then the current depth is the desired one + desiredDepth[depth - 1] <== inverse(isZero[depth - 2].out); + done[depth - 2] <== desiredDepth[depth - 1]; + + // desiredDepth will be `1` the first time we encounter non-zero branch on the previous depth + for (var i = depth - 2; i > 0; i--) { + desiredDepth[i] <== inverse(done[i]) * inverse(isZero[i - 1].out); + done[i - 1] <== desiredDepth[i] + done[i]; + } + + desiredDepth[0] <== inverse(done[0]); +} + +/* + * Determines the type of the node + */ +template NodeTypeDeterminer() { + signal input auxIsEmpty; + // 1 if the node is at the desired depth, 0 otherwise + signal input isDesiredDepth; + signal input isExclusion; + + signal input previousMiddle; + signal input previousEmpty; + signal input previousAuxLeaf; + signal input previousLeaf; + + // 1 if the node is a middle node, 0 otherwise + signal output middle; + // 1 if the node is an empty node, 0 otherwise + signal output empty; + // 1 if the node is a leaf node for the exclusion proof, 0 otherwise + signal output auxLeaf; + // 1 if the node is a leaf node, 0 otherwise + signal output leaf; + + // 1 if the node is a leaf node and we are checking for exclusion, 0 otherwise + signal leafForExclusionCheck <== isDesiredDepth * isExclusion; + + // Determine the node as a middle, until getting to the desired depth + middle <== previousMiddle - isDesiredDepth; + + // Determine the node as a leaf, when we are at the desired depth and + // we check for inclusion + leaf <== isDesiredDepth - leafForExclusionCheck; + + // Determine the node as an auxLeaf, when we are at the desired depth and + // we check for exclusion in a bamboo scenario + auxLeaf <== leafForExclusionCheck * inverse(auxIsEmpty); + + // Determine the node as an empty, when we are at the desired depth and + // we check for exclusion with an empty node + empty <== isDesiredDepth * auxIsEmpty; +} + +/* + * Gets hash at the current depth, based on the type of the node + * If the mode is a empty, then the hash is 0 + */ +template DepthHasher() { + signal input isMiddle; + signal input isAuxLeaf; + signal input isLeaf; + + signal input sibling; + signal input auxLeaf; + signal input leaf; + signal input currentKeyBit; + signal input child; + + signal output root; + + component switcher = Switcher(); + switcher.L <== child; + switcher.R <== sibling; + // Based on the current key bit, we understand which order to use + switcher.sel <== currentKeyBit; + + component proofHash = Hash2(); + proofHash.a <== switcher.outL; + proofHash.b <== switcher.outR; + + signal res[3]; + // hash of the middle node + res[0] <== proofHash.out * isMiddle; + // hash of the aux leaf node for the exclusion proof + res[1] <== auxLeaf * isAuxLeaf; + // hash of the leaf node for the inclusion proof + res[2] <== leaf * isLeaf; + + // only one of the following will be non-zero + root <== res[0] + res[1] + res[2]; +} + +/* + * Checks the sparse merkle proof against the given root + */ +template SparseMerkleTree(depth) { + // The root of the sparse merkle tree + signal input root; + // The siblings for each depth + signal input siblings[depth]; + + signal input key; + signal input value; + + signal input auxKey; + signal input auxValue; + // 1 if the aux node is empty, 0 otherwise + signal input auxIsEmpty; + + // 1 if we are checking for exclusion, 0 if we are checking for inclusion + signal input isExclusion; + + // Check that the auxIsEmpty is 0 if we are checking for inclusion + component exclusiveCase = AND(); + exclusiveCase.a <== inverse(isExclusion); + exclusiveCase.b <== auxIsEmpty; + exclusiveCase.out === 0; + + // Check that the key != auxKey if we are checking for exclusion and the auxIsEmpty is 0 + component areKeyEquals = IsEqual(); + areKeyEquals.in[0] <== auxKey; + areKeyEquals.in[1] <== key; + + component keysOk = MultiAND(3); + keysOk.in[0] <== isExclusion; + keysOk.in[1] <== inverse(auxIsEmpty); + keysOk.in[2] <== areKeyEquals.out; + keysOk.out === 0; + + component auxHash = Hash3(); + auxHash.a <== auxKey; + auxHash.b <== auxValue; + auxHash.c <== 1; + + component hash = Hash3(); + hash.a <== key; + hash.b <== value; + hash.c <== 1; + + component keyBits = Num2Bits_strict(); + keyBits.in <== key; + + component depths = DepthDeterminer(depth); + + for (var i = 0; i < depth; i++) { + depths.siblings[i] <== siblings[i]; + } + + component nodeType[depth]; + + // Start with the middle node (closest to the root) + for (var i = 0; i < depth; i++) { + nodeType[i] = NodeTypeDeterminer(); + + if (i == 0) { + nodeType[i].previousMiddle <== 1; + nodeType[i].previousEmpty <== 0; + nodeType[i].previousLeaf <== 0; + nodeType[i].previousAuxLeaf <== 0; + } else { + nodeType[i].previousMiddle <== nodeType[i - 1].middle; + nodeType[i].previousEmpty <== nodeType[i - 1].empty; + nodeType[i].previousLeaf <== nodeType[i - 1].leaf; + nodeType[i].previousAuxLeaf <== nodeType[i - 1].auxLeaf; + } + + nodeType[i].auxIsEmpty <== auxIsEmpty; + nodeType[i].isExclusion <== isExclusion; + nodeType[i].isDesiredDepth <== depths.desiredDepth[i]; + } + + component depthHash[depth]; + + // Hash up the elements in the reverse order + for (var i = depth - 1; i >= 0; i--) { + depthHash[i] = DepthHasher(); + + depthHash[i].isMiddle <== nodeType[i].middle; + depthHash[i].isLeaf <== nodeType[i].leaf; + depthHash[i].isAuxLeaf <== nodeType[i].auxLeaf; + + depthHash[i].sibling <== siblings[i]; + depthHash[i].auxLeaf <== auxHash.out; + depthHash[i].leaf <== hash.out; + + depthHash[i].currentKeyBit <== keyBits.out[i]; + + if (i == depth - 1) { + // The last depth has no child + depthHash[i].child <== 0; + } else { + // The child of the current depth is the root of the next depth + depthHash[i].child <== depthHash[i + 1].root; + } + } + + // The root of the merkle tree is the root of the first depth + depthHash[0].root === root; +} diff --git a/assets/erc-7812/contracts/SparseMerkleTree.sol b/assets/erc-7812/contracts/SparseMerkleTree.sol new file mode 100644 index 0000000000..59325c4d04 --- /dev/null +++ b/assets/erc-7812/contracts/SparseMerkleTree.sol @@ -0,0 +1,671 @@ +// SPDX-License-Identifier: CC0-1.0 +pragma solidity ^0.8.21; + +import {IEvidenceDB} from "../interfaces/IEvidenceDB.sol"; + +/** + * @notice Sparse Merkle Tree implementation. + */ +library SparseMerkleTree { + /** + * @dev A maximum depth hard cap for SMT + * Due to the limitations of the uint256 data type, depths greater than 256 are not possible. + */ + uint16 internal constant MAX_DEPTH_HARD_CAP = 256; + + uint64 internal constant ZERO_IDX = 0; + + bytes32 internal constant ZERO_HASH = bytes32(0); + + /** + * @notice The type of the node in the Merkle tree. + */ + enum NodeType { + EMPTY, + LEAF, + MIDDLE + } + + /** + * @notice Defines the structure of the Sparse Merkle Tree. + * + * @param nodes A mapping of the tree's nodes, where the key is the node's index, starting from 1 upon node addition. + * This approach differs from the original implementation, which utilized a hash as the key: + * H(k || v || 1) for leaf nodes and H(left || right) for middle nodes. + * + * @param merkleRootId The index of the root node. + * @param maxDepth The maximum depth of the Merkle tree. + * @param nodesCount The total number of nodes within the Merkle tree. + * @param customHasherSet Indicates whether custom hash functions have been configured (true) or not (false). + * @param hash2 A hash function accepting two arguments. + * @param hash3 A hash function accepting three arguments. + */ + struct SMT { + mapping(uint256 => Node) nodes; + uint64 merkleRootId; + uint64 nodesCount; + uint64 deletedNodesCount; + uint32 maxDepth; + bool customHasherSet; + function(bytes32, bytes32) view returns (bytes32) hash2; + function(bytes32, bytes32, bytes32) view returns (bytes32) hash3; + } + + /** + * @notice Describes a node within the Merkle tree, including its type, children, hash, and key-value pair. + * + * @param nodeType The type of the node. + * @param childLeft The index of the left child node. + * @param childRight The index of the right child node. + * @param nodeHash The hash of the node, calculated as follows: + * - For leaf nodes, H(k || v || 1) where k is the key and v is the value; + * - For middle nodes, H(left || right) where left and right are the hashes of the child nodes. + * + * @param key The key associated with the node. + * @param value The value associated with the node. + */ + struct Node { + NodeType nodeType; + uint64 childLeft; + uint64 childRight; + bytes32 nodeHash; + bytes32 key; + bytes32 value; + } + + modifier onlyInitialized(SMT storage tree) { + if (!_isInitialized(tree)) revert TreeNotInitialized(); + _; + } + + error KeyAlreadyExists(bytes32 key); + error LeafDoesNotMatch(bytes32 currentKey, bytes32 key); + error MaxDepthExceedsHardCap(uint32 maxDepth); + error MaxDepthIsZero(); + error MaxDepthReached(); + error NewMaxDepthMustBeLarger(uint32 currentDepth, uint32 newDepth); + error NodeDoesNotExist(uint256 nodeId); + error TreeAlreadyInitialized(); + error TreeNotInitialized(); + error TreeIsNotEmpty(); + + /** + * @notice The function to initialize the Merkle tree. + * Under the hood it sets the maximum depth of the Merkle tree, therefore can be considered + * alias function for the `setMaxDepth`. + * + * Requirements: + * - The current tree depth must be 0. + * + * @param tree self. + * @param maxDepth_ The max depth of the Merkle tree. + */ + function initialize(SMT storage tree, uint32 maxDepth_) internal { + if (_isInitialized(tree)) revert TreeAlreadyInitialized(); + + _setMaxDepth(tree, maxDepth_); + } + + /** + * @notice The function to set the maximum depth of the Merkle tree. Complexity is O(1). + * + * Requirements: + * - The max depth must be greater than zero. + * - The max depth can only be increased. + * - The max depth is less than or equal to MAX_DEPTH_HARD_CAP (256). + * + * @param tree self. + * @param maxDepth_ The max depth of the Merkle tree. + */ + function setMaxDepth(SMT storage tree, uint32 maxDepth_) internal { + _setMaxDepth(tree, maxDepth_); + } + + /** + * @notice The function to set a custom hash functions, that will be used to build the Merkle Tree. + * + * Requirements: + * - The tree must be empty. + * + * @param tree self. + * @param hash2_ The hash function that accepts two argument. + * @param hash3_ The hash function that accepts three arguments. + */ + function setHashers( + SMT storage tree, + function(bytes32, bytes32) view returns (bytes32) hash2_, + function(bytes32, bytes32, bytes32) view returns (bytes32) hash3_ + ) internal { + if (_nodesCount(tree) != 0) revert TreeIsNotEmpty(); + + tree.customHasherSet = true; + + tree.hash2 = hash2_; + tree.hash3 = hash3_; + } + + /** + * @notice The function to add a new element to the bytes32 tree. + * Complexity is O(log(n)), where n is the max depth of the tree. + * + * @param tree self. + * @param key_ The key of the element. + * @param value_ The value of the element. + */ + function add(SMT storage tree, bytes32 key_, bytes32 value_) internal onlyInitialized(tree) { + _add(tree, key_, value_); + } + + /** + * @notice The function to remove a (leaf) element from the bytes32 tree. + * Complexity is O(log(n)), where n is the max depth of the tree. + * + * @param tree self. + * @param key_ The key of the element. + */ + function remove(SMT storage tree, bytes32 key_) internal onlyInitialized(tree) { + tree.merkleRootId = uint64(_remove(tree, key_, tree.merkleRootId, 0)); + } + + /** + * @notice The function to update a (leaf) element in the bytes32 tree. + * Complexity is O(log(n)), where n is the max depth of the tree. + * + * @param tree self. + * @param key_ The key of the element. + * @param newValue_ The new value of the element. + */ + function update( + SMT storage tree, + bytes32 key_, + bytes32 newValue_ + ) internal onlyInitialized(tree) { + _update(tree, key_, newValue_); + } + + /** + * @notice The function to get the proof if a node with specific key exists or not exists in the SMT. + * Complexity is O(log(n)), where n is the max depth of the tree. + * + * @param tree self. + * @param key_ The key of the element. + * @return SMT proof struct. + */ + function getProof( + SMT storage tree, + bytes32 key_ + ) internal view returns (IEvidenceDB.Proof memory) { + uint256 maxDepth_ = _maxDepth(tree); + + IEvidenceDB.Proof memory proof_ = IEvidenceDB.Proof({ + root: _root(tree), + siblings: new bytes32[](maxDepth_), + existence: false, + key: key_, + value: ZERO_HASH, + auxExistence: false, + auxKey: ZERO_HASH, + auxValue: ZERO_HASH + }); + + Node memory node_; + uint256 nextNodeId_ = tree.merkleRootId; + + for (uint256 i = 0; i <= maxDepth_; i++) { + node_ = _node(tree, nextNodeId_); + + if (node_.nodeType == NodeType.EMPTY) { + break; + } else if (node_.nodeType == NodeType.LEAF) { + if (node_.key == proof_.key) { + proof_.existence = true; + proof_.value = node_.value; + + break; + } else { + proof_.auxExistence = true; + proof_.auxKey = node_.key; + proof_.auxValue = node_.value; + proof_.value = node_.value; + + break; + } + } else { + if ((uint256(proof_.key) >> i) & 1 == 1) { + nextNodeId_ = node_.childRight; + + proof_.siblings[i] = tree.nodes[node_.childLeft].nodeHash; + } else { + nextNodeId_ = node_.childLeft; + + proof_.siblings[i] = tree.nodes[node_.childRight].nodeHash; + } + } + } + + return proof_; + } + + /** + * @notice The function to get the root of the Merkle tree. + * Complexity is O(1). + * + * @param tree self. + * @return The root of the Merkle tree. + */ + function getRoot(SMT storage tree) internal view returns (bytes32) { + return _root(tree); + } + + /** + * @notice The function to get the node by its index. + * Complexity is O(1). + * + * @param tree self. + * @param nodeId_ The index of the node. + * @return The node. + */ + function getNode(SMT storage tree, uint256 nodeId_) internal view returns (Node memory) { + return _node(tree, nodeId_); + } + + /** + * @notice The function to get the node by its key. + * Complexity is O(log(n)), where n is the max depth of the tree. + * + * @param tree self. + * @param key_ The key of the element. + * @return The node. + */ + function getNodeByKey(SMT storage tree, bytes32 key_) internal view returns (Node memory) { + Node memory node_; + uint256 nextNodeId_ = tree.merkleRootId; + + for (uint256 i = 0; i <= tree.maxDepth; i++) { + node_ = tree.nodes[nextNodeId_]; + + if (node_.nodeType == NodeType.EMPTY) { + break; + } else if (node_.nodeType == NodeType.LEAF) { + if (node_.key == key_) { + break; + } + } else { + if ((uint256(key_) >> i) & 1 == 1) { + nextNodeId_ = node_.childRight; + } else { + nextNodeId_ = node_.childLeft; + } + } + } + + return + node_.key == key_ + ? node_ + : Node({ + nodeType: NodeType.EMPTY, + childLeft: ZERO_IDX, + childRight: ZERO_IDX, + nodeHash: ZERO_HASH, + key: ZERO_HASH, + value: ZERO_HASH + }); + } + + /** + * @notice The function to get the max depth of the Merkle tree. + * + * @param tree self. + * @return The max depth of the Merkle tree. + */ + function getMaxDepth(SMT storage tree) internal view returns (uint64) { + return uint64(_maxDepth(tree)); + } + + /** + * @notice The function to get the number of nodes in the Merkle tree. + * + * @param tree self. + * @return The number of nodes in the Merkle tree. + */ + function getNodesCount(SMT storage tree) internal view returns (uint64) { + return uint64(_nodesCount(tree)); + } + + /** + * @notice The function to check if custom hash functions are set. + * + * @param tree self. + * @return True if custom hash functions are set, otherwise false. + */ + function isCustomHasherSet(SMT storage tree) internal view returns (bool) { + return tree.customHasherSet; + } + + function _setMaxDepth(SMT storage tree, uint32 maxDepth_) private { + if (maxDepth_ == 0) revert MaxDepthIsZero(); + + uint32 currentDepth_ = tree.maxDepth; + + if (maxDepth_ <= currentDepth_) revert NewMaxDepthMustBeLarger(currentDepth_, maxDepth_); + if (maxDepth_ > MAX_DEPTH_HARD_CAP) revert MaxDepthExceedsHardCap(maxDepth_); + + tree.maxDepth = maxDepth_; + } + + function _add(SMT storage tree, bytes32 key_, bytes32 value_) private { + Node memory node_ = Node({ + nodeType: NodeType.LEAF, + childLeft: ZERO_IDX, + childRight: ZERO_IDX, + nodeHash: ZERO_HASH, + key: key_, + value: value_ + }); + + tree.merkleRootId = uint64(_add(tree, node_, tree.merkleRootId, 0)); + } + + function _update(SMT storage tree, bytes32 key_, bytes32 newValue_) private { + Node memory node_ = Node({ + nodeType: NodeType.LEAF, + childLeft: ZERO_IDX, + childRight: ZERO_IDX, + nodeHash: ZERO_HASH, + key: key_, + value: newValue_ + }); + + _update(tree, node_, tree.merkleRootId, 0); + } + + /** + * @dev The check for whether the current depth exceeds the maximum depth is omitted for two reasons: + * 1. The current depth may only surpass the maximum depth during the addition of a new leaf. + * 2. As we navigate through middle nodes, the current depth is assured to remain below the maximum + * depth since the traversal must ultimately conclude at a leaf node. + */ + function _add( + SMT storage tree, + Node memory newLeaf_, + uint256 nodeId_, + uint16 currentDepth_ + ) private returns (uint256) { + Node memory currentNode_ = tree.nodes[nodeId_]; + + if (currentNode_.nodeType == NodeType.EMPTY) { + return _setNode(tree, newLeaf_); + } else if (currentNode_.nodeType == NodeType.LEAF) { + if (currentNode_.key == newLeaf_.key) revert KeyAlreadyExists(newLeaf_.key); + + return _pushLeaf(tree, newLeaf_, currentNode_, nodeId_, currentDepth_); + } else { + uint256 nextNodeId_; + + if ((uint256(newLeaf_.key) >> currentDepth_) & 1 == 1) { + nextNodeId_ = _add(tree, newLeaf_, currentNode_.childRight, currentDepth_ + 1); + + tree.nodes[nodeId_].childRight = uint64(nextNodeId_); + } else { + nextNodeId_ = _add(tree, newLeaf_, currentNode_.childLeft, currentDepth_ + 1); + + tree.nodes[nodeId_].childLeft = uint64(nextNodeId_); + } + + tree.nodes[nodeId_].nodeHash = _getNodeHash(tree, tree.nodes[nodeId_]); + + return nodeId_; + } + } + + function _remove( + SMT storage tree, + bytes32 key_, + uint256 nodeId_, + uint16 currentDepth_ + ) private returns (uint256) { + Node memory currentNode_ = tree.nodes[nodeId_]; + + if (currentNode_.nodeType == NodeType.EMPTY) { + revert NodeDoesNotExist(nodeId_); + } else if (currentNode_.nodeType == NodeType.LEAF) { + if (currentNode_.key != key_) revert LeafDoesNotMatch(currentNode_.key, key_); + + _deleteNode(tree, nodeId_); + + return ZERO_IDX; + } else { + uint256 nextNodeId_; + + if ((uint256(key_) >> currentDepth_) & 1 == 1) { + nextNodeId_ = _remove(tree, key_, currentNode_.childRight, currentDepth_ + 1); + } else { + nextNodeId_ = _remove(tree, key_, currentNode_.childLeft, currentDepth_ + 1); + } + + NodeType rightType_ = tree.nodes[currentNode_.childRight].nodeType; + NodeType leftType_ = tree.nodes[currentNode_.childLeft].nodeType; + + if (rightType_ == NodeType.EMPTY && leftType_ == NodeType.EMPTY) { + _deleteNode(tree, nodeId_); + + return nextNodeId_; + } + + NodeType nextType_ = tree.nodes[nextNodeId_].nodeType; + + if ( + (rightType_ == NodeType.EMPTY || leftType_ == NodeType.EMPTY) && + nextType_ != NodeType.MIDDLE + ) { + if ( + nextType_ == NodeType.EMPTY && + (leftType_ == NodeType.LEAF || rightType_ == NodeType.LEAF) + ) { + _deleteNode(tree, nodeId_); + + if (rightType_ == NodeType.LEAF) { + return currentNode_.childRight; + } + + return currentNode_.childLeft; + } + + if (rightType_ == NodeType.EMPTY) { + tree.nodes[nodeId_].childRight = uint64(nextNodeId_); + } else { + tree.nodes[nodeId_].childLeft = uint64(nextNodeId_); + } + } + + tree.nodes[nodeId_].nodeHash = _getNodeHash(tree, tree.nodes[nodeId_]); + + return nodeId_; + } + } + + function _update( + SMT storage tree, + Node memory newLeaf_, + uint256 nodeId_, + uint16 currentDepth_ + ) private { + Node memory currentNode_ = tree.nodes[nodeId_]; + + if (currentNode_.nodeType == NodeType.EMPTY) { + revert NodeDoesNotExist(nodeId_); + } else if (currentNode_.nodeType == NodeType.LEAF) { + if (currentNode_.key != newLeaf_.key) + revert LeafDoesNotMatch(currentNode_.key, newLeaf_.key); + + tree.nodes[nodeId_] = newLeaf_; + currentNode_ = newLeaf_; + } else { + if ((uint256(newLeaf_.key) >> currentDepth_) & 1 == 1) { + _update(tree, newLeaf_, currentNode_.childRight, currentDepth_ + 1); + } else { + _update(tree, newLeaf_, currentNode_.childLeft, currentDepth_ + 1); + } + } + + tree.nodes[nodeId_].nodeHash = _getNodeHash(tree, currentNode_); + } + + function _pushLeaf( + SMT storage tree, + Node memory newLeaf_, + Node memory oldLeaf_, + uint256 oldLeafId_, + uint16 currentDepth_ + ) private returns (uint256) { + if (currentDepth_ >= tree.maxDepth) revert MaxDepthReached(); + + Node memory newNodeMiddle_; + bool newLeafBitAtDepth_ = (uint256(newLeaf_.key) >> currentDepth_) & 1 == 1; + bool oldLeafBitAtDepth_ = (uint256(oldLeaf_.key) >> currentDepth_) & 1 == 1; + + // Check if we need to go deeper if diverge at the depth's bit + if (newLeafBitAtDepth_ == oldLeafBitAtDepth_) { + uint256 nextNodeId_ = _pushLeaf( + tree, + newLeaf_, + oldLeaf_, + oldLeafId_, + currentDepth_ + 1 + ); + + if (newLeafBitAtDepth_) { + // go right + newNodeMiddle_ = Node({ + nodeType: NodeType.MIDDLE, + childLeft: ZERO_IDX, + childRight: uint64(nextNodeId_), + nodeHash: ZERO_HASH, + key: ZERO_HASH, + value: ZERO_HASH + }); + } else { + // go left + newNodeMiddle_ = Node({ + nodeType: NodeType.MIDDLE, + childLeft: uint64(nextNodeId_), + childRight: ZERO_IDX, + nodeHash: ZERO_HASH, + key: ZERO_HASH, + value: ZERO_HASH + }); + } + + return _setNode(tree, newNodeMiddle_); + } + + uint256 newLeafId = _setNode(tree, newLeaf_); + + if (newLeafBitAtDepth_) { + newNodeMiddle_ = Node({ + nodeType: NodeType.MIDDLE, + childLeft: uint64(oldLeafId_), + childRight: uint64(newLeafId), + nodeHash: ZERO_HASH, + key: ZERO_HASH, + value: ZERO_HASH + }); + } else { + newNodeMiddle_ = Node({ + nodeType: NodeType.MIDDLE, + childLeft: uint64(newLeafId), + childRight: uint64(oldLeafId_), + nodeHash: ZERO_HASH, + key: ZERO_HASH, + value: ZERO_HASH + }); + } + + return _setNode(tree, newNodeMiddle_); + } + + /** + * @dev The function used to add new nodes. + */ + function _setNode(SMT storage tree, Node memory node_) private returns (uint256) { + node_.nodeHash = _getNodeHash(tree, node_); + + uint256 newCount_ = ++tree.nodesCount; + tree.nodes[newCount_] = node_; + + return newCount_; + } + + /** + * @dev The function used to delete removed nodes. + */ + function _deleteNode(SMT storage tree, uint256 nodeId_) private { + delete tree.nodes[nodeId_]; + ++tree.deletedNodesCount; + } + + /** + * @dev The check for an empty node is omitted, as this function is called only with + * non-empty nodes and is not intended for external use. + */ + function _getNodeHash(SMT storage tree, Node memory node_) private view returns (bytes32) { + function(bytes32, bytes32) view returns (bytes32) hash2_ = tree.customHasherSet + ? tree.hash2 + : _hash2; + function(bytes32, bytes32, bytes32) view returns (bytes32) hash3_ = tree.customHasherSet + ? tree.hash3 + : _hash3; + + if (node_.nodeType == NodeType.LEAF) { + return hash3_(node_.key, node_.value, bytes32(uint256(1))); + } + + return hash2_(tree.nodes[node_.childLeft].nodeHash, tree.nodes[node_.childRight].nodeHash); + } + + function _hash2(bytes32 a, bytes32 b) private pure returns (bytes32 result) { + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + mstore(0, a) + mstore(32, b) + + result := keccak256(0, 64) + } + } + + /** + * @dev The decision not to update the free memory pointer is due to the temporary nature of the hash arguments. + */ + function _hash3(bytes32 a, bytes32 b, bytes32 c) private pure returns (bytes32 result) { + // solhint-disable-next-line no-inline-assembly + assembly ("memory-safe") { + let free_ptr := mload(64) + + mstore(free_ptr, a) + mstore(add(free_ptr, 32), b) + mstore(add(free_ptr, 64), c) + + result := keccak256(free_ptr, 96) + } + } + + function _root(SMT storage tree) private view returns (bytes32) { + return tree.nodes[tree.merkleRootId].nodeHash; + } + + function _node(SMT storage tree, uint256 nodeId_) private view returns (Node memory) { + return tree.nodes[nodeId_]; + } + + function _maxDepth(SMT storage tree) private view returns (uint256) { + return tree.maxDepth; + } + + function _nodesCount(SMT storage tree) private view returns (uint256) { + return tree.nodesCount - tree.deletedNodesCount; + } + + function _isInitialized(SMT storage tree) private view returns (bool) { + return tree.maxDepth > 0; + } +} diff --git a/assets/erc-7812/images/diagram.png b/assets/erc-7812/images/diagram.png new file mode 100644 index 0000000000..4f1eb9948e Binary files /dev/null and b/assets/erc-7812/images/diagram.png differ