# Revocation EIP Braindump We want to propose a new EIP that standardizes a new and simple revocation scheme on Ethereum. It is intended to store less data on-chain compared to [EthrStatusRegistry2019 (uPort, stores 32 byte digest)](https://github.com/uport-project/revocation-registry) and be as simple as [StatusList2021](https://w3c-ccg.github.io/vc-status-list-2021/). ## Table of contents 1. Requirements 2. Revocation Mechanism 3. Revocation Contract Methods 4. Upgrade Mechanism 5. Governance Mechanism 6. Open Questions ## 1. Requirements The EIP should define the interface of a (number of) smart contract(s) that follow these requirements: 1. Upgradeable: The code should be upgradable to introduce new features or fix bugs. Design decision have to be made so that the storage (revocation lists) can be migrated to a new contract. 2. Pauseable: It should be possible to pause all writing actions through the proxy, e.g., while migrating to a new contract. 3. Migration: We may want to migrate data from an old contract to a new one. Let's also think about: - scope out some parts of the data, e.g., due to a hack - how to migrate mappings (events?) and arrays - do we do on-chain or off-chain migrations? 4. Address: The contract address should be constant for a long time, even after upgrades. 5. Storage needs: Stored data should be kept at a minimum to keep transactions costs low. 6. **Simple***: The revocation algorithm and the contract itself should be easy to understand. 7. Governance: A mechanism should be found to govern contract upgrades. 8. **Registry***: The contract should be the registry for a multitude of revocation lists. An address may own and manage multiple revocation lists. 9. **Meta Transaction***: A revocation list owner may enable some third party to do txs for them. (this enables a product :))) 10. Constant cost: Doing changes in the registry should have constant costs in relation to the number of managed revocation lists. 11. **Delegation**: A revocation list owner might enable another address write access. - Example: Legisym might want to give us the ability to also do revocations on their list -> they can add us as an delegate 13. **Owner change**: A revocation list owner might change the owner of one of its list. - the pointer inside the VC should still resolve to the new location in the contract - for security reason, an owner might want to exchange its address to a new one due to pk leak \* will be part of the EIP **Comments from Carsten:** * How does it work to use multiple *revocation lists*? * What about *extensibility*? (e.g. extended data structure including reason for revocation code, successor credential). * What happens if we want to scale even more and we add new forms of *cryptographic data structures*? (e.g. sparse merkle trees) * How could the more sophisticated data structures be integrated with *L2 scaling*? * Can we do a comparision with *cryptographic aggregators* in Indy? (maybe not) ## 2. Revocation Mechanism Each credential is assigned a unique `uint256` value, called the revocation key. The revocation key is also the key in a revocation list `mapping(uint256 => bool)` that points to a boolean value describing the revocation status of its underlying VC. To store multiple revocation lists for an owner`address`, we should create a *multidimensional* `mapping`. This could look like this: ```solidity mapping(address => mapping(uint256 => mapping(uint256 => bool))) ^ ^ ^ |_ owner address | | |_ list nr | |_ revocation key ``` Instead of a list nr, we could use `bytes32` to allow users to set a short human-meaningful name. *Carsten:* Could this be combined with ENS? ## 3. Revocation Contract Methods We need a multitide of methods to enable the function requirements. **Get revocation status of key in list** ```solidity function isRevoked(address owner, uint256 list, uint256 revocationKey) public view returns (bool) ``` **Change status of a key in a revocation list** ```solidity function changeStatus(bool revoked, uint256 list, uint256 revocationKey) public ``` **Change status of a key in a revocation list via meta transaction** ```solidity function changeStatusSigned(bool revoked, uint256 list, uint256 revocationKey, uint8 sigV, bytes32 sigR, bytes32 sigS) public ``` **Change status of a key in a revocation list as delegate** ```solidity function changeStatusDelegated(bool revoked, uint256 list, uint256 revocationKey) public ``` **Change status of a key in a revocation list as delegate via meta transaction** ```solidity function changeStatusDelegatedSigned(bool revoked, uint256 list, uint256 revocationKey, uint8 sigV, bytes32 sigR, bytes32 sigS) public ``` **Change owner** ```solidity function changeListOwner(address newOwner, uint256 list) public ``` **Change owner via meta transaction** ```solidity function changeListOwnerSigned(address newOwner, address owner, uint256 list, uint8 sigV, bytes32 sigR, bytes32 sigS) public ``` **Add delegate** ```solidity function addDelegate(address delegate, uint256 list, uint validity) public ``` **Add delegate via meta transaction** ```solidity function addListDelegateSigned(address delegate, address owner, uint256 list, uint validity, uint8 sigV, bytes32 sigR, bytes32 sigS) public ``` **Remove delegate** ```solidity function removeListDelegate(address delegate, uint256 list, uint validity) public ``` **Remove delegate via meta transaction** ```solidity function removeListDelegateSigned(address delegate, address owner, uint256 list, uint validity, uint8 sigV, bytes32 sigR, bytes32 sigS) public ``` We could even think about also emitting events for every change with all used mapping keys, as this could be helpful to re-build the state for future migrations. Or just for wallets to track the status of a credential. ## 4. Upgrade Mechanism We could make use of proxy contracts. Contract callers always call the actual contract through a proxy contract. This enables us to switch out the contract logic while maintaining a constant contract address. OpenZepplin has stuff for that. We can also think about separating into logic and storage to keep all revocation data even if we switch out the logic. Something like that: ```graphviz digraph hierarchy { nodesep=1.0 node [color=Black,fontname=Courier,shape=box] edge [color=Blue, style=dashed] Caller->{ProxyContract} ProxyContract->{Caller} ProxyContract->{TokenContract} ProxyContract->{LogicContract} LogicContract->{ProxyContract} TokenContract->{ProxyContract} LogicContract->{OldStorageContract} OldStorageContract->{LogicContract} StorageContract->{LogicContract} LogicContract->{StorageContract} {rank=same;ProxyContract TokenContract} } ``` Throwing in a token contract and putting governance into the proxy contract allows for DAO stuff. ### How can we do migrations? Arrays are easy, we can access them directly between contracts Mappings are not easy. How do we do it? 1. Query events that contain keys, get values off-chain, and give keys and values in constructor (costly, think about breaking up the batch process) 2. Query events that contain keys, allow new contract in constructor to directy access storage of old contract (a lot of hashing, costly!) 3. Split logic and storage into two contracts ## 5. Governance Mechanism *tbd* OpenZepplin has contracts for that that can be used in combination with an ERC20/721 token. We have to evaluate if the complexity and cost is worth it. ## 6. Open Questions - [ ] What format should the hash for signed methods have? - [ ] Do we want to include the DAO in the EIP or keep it pure? - [ ] Do we want to include the upgrade mechanism in the EIP or keep it pure? - [ ] Do we want to do delegation and owner changes? (not sure how to do it elegantly) - [ ] How can people join the DAO? *Carsten*: If we do a DAO, shouldn't we connect it with the DID:Ethr DAO? - [ ] Do we want to focus this EIP on SSI or should it be generic - [ ] Is there a place where we can impose a fee for the DAO (we may need a fund for funding migration txs; should not be too expensive) ... *Carsten*: I like this idea a lot. - [ ] Should we create some modifier to allow "de-revoke"? - [ ] Can we migrate mappings without using events in the constructor? # EIP Template https://github.com/ethereum/EIPs/blob/master/eip-template.md # SOL ```solidity= // SPDX-License-Identifier: GPL-3.0 pragma solidity >=0.7.0 <0.9.0; contract RevocationRegistry { // Revocations happen in revocation lists that belong to an address/ user namespace mapping(address => mapping(bytes32 => mapping(bytes32 => bool))) registry; // New Owners: Incase an owner has changed the owner of one of the lists in its namespaces // It acts like a symlink of one namespace/ address to a list inside another namespace // (hash(ownerNamespace, list) => newOwner => bool mapping(bytes32 => mapping(address => bool)) newOwners; // Delegates: A namespacer owner can add access to one of its lists to another namespace/ address // Acts as a lookup table of what addresses have delegate access to what revocation list in which namespaces // (hash(ownerNamespace, list) => newOwner => expiryTiemstamp mapping(bytes32 => mapping(address => uint)) delegates; // Nonce tracking for meta transactions mapping(address => uint) nonces; constructor() {} function isRevoked(address namespace, bytes32 list, bytes32 revocationKey) public view returns (bool) { return (registry[namespace][list][revocationKey]); } function changeStatus(bool revoked, address namespace, bytes32 list, bytes32 revocationKey) isOwner(namespace, list) public { registry[namespace][list][revocationKey] = revoked; // emit Event } function changeStatusesInList(bool[] memory revoked, address namespace, bytes32 list, bytes32[] memory revocationKeys) isOwner(namespace, list) public { for (uint i = 0; i < revoked.length; i++) { changeStatus(revoked[i], namespace, list, revocationKeys[i]); } } function changeStatusDelegated(bool revoked, address namespace, bytes32 list, bytes32 revocationKey) isDelegate(namespace, list) public { registry[namespace][list][revocationKey] = revoked; } function changeListOwner(address namespace, address newOwner, bytes32 list) isOwner(namespace, list) public { bytes32 listLocationHash = generateListLocationHash(namespace, list); // Remove current owner (caller) and set new one newOwners[listLocationHash][msg.sender] = false; newOwners[listLocationHash][newOwner] = true; } function addListDelegate(address namespace, address delegate, bytes32 list, uint validity) isOwner(namespace, list) public { bytes32 listLocationHash = generateListLocationHash(namespace, list); delegates[listLocationHash][delegate] = validity; } function removeListDelegate(address namespace, address delegate, bytes32 list) isOwner(namespace, list) public { bytes32 listLocationHash = generateListLocationHash(namespace, list); delegates[listLocationHash][delegate] = 0; } function generateListLocationHash(address namespace, bytes32 list) pure internal returns(bytes32) { return keccak256(abi.encodePacked(namespace, list)); } // Check if // - caller is acting in its namespace // - or they got owner rights in a foreign namespace modifier isOwner(address namespace, bytes32 list) { bytes32 listLocationHash = generateListLocationHash(namespace, list); if (!newOwners[listLocationHash][msg.sender]) { require(msg.sender == namespace, "Caller is not an owner"); } _; } // Check if caller got delegate rights in a foreign namespace before expiry modifier isDelegate(address namespace, bytes32 list) { bytes32 listLocationHash = generateListLocationHash(namespace, list); require(delegates[listLocationHash][msg.sender] > block.timestamp, "Caller is not a delegate"); _; } } ```