@coordination-network/contracts 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ApprovedIssuerSet.sol +630 -0
- package/CredentialRevocationRegistry.sol +203 -0
- package/ERC5564Announcer.sol +17 -0
- package/IdentityRegistry.sol +121 -0
- package/LICENSE +21 -0
- package/NetworkMemberGovernance.sol +264 -0
- package/NetworkMemberGroup.sol +74 -0
- package/NetworkMemberRegistry.sol +217 -0
- package/README.md +92 -0
- package/SemaphorePaymaster.sol +200 -0
- package/base/NetworkMemberGate.sol +77 -0
- package/interfaces/IApprovedIssuerSet.sol +16 -0
- package/interfaces/IERC5564Announcer.sol +38 -0
- package/interfaces/ISafeGovernance.sol +34 -0
- package/libraries/IssuerSetHashes.sol +35 -0
- package/package.json +43 -0
- package/revocation/README.md +86 -0
- package/revocation/Revocation.sol +187 -0
- package/revocation/base/RevocationRegistries.sol +230 -0
- package/revocation/interfaces/IRevocation.sol +125 -0
- package/revocation/interfaces/IRevocationRegistries.sol +122 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.34;
|
|
3
|
+
|
|
4
|
+
/// @title IApprovedIssuerSet
|
|
5
|
+
/// @notice The one call the NetworkMemberRegistry makes into the approved-issuer set.
|
|
6
|
+
/// @dev Exists only to break the import cycle: the set reads the registry for authorization, and the
|
|
7
|
+
/// registry calls the set when it removes a member. Deliberately not the full ABI.
|
|
8
|
+
interface IApprovedIssuerSet {
|
|
9
|
+
/// @notice Retires every signing key a member has published.
|
|
10
|
+
/// @param memberNetworkId The member whose keys are being retired.
|
|
11
|
+
function retireAllIssuerKeys(bytes32 memberNetworkId) external;
|
|
12
|
+
|
|
13
|
+
/// @notice The registry this set answers, so the registry can refuse to wire a set built for
|
|
14
|
+
/// another one.
|
|
15
|
+
function memberRegistry() external view returns (address);
|
|
16
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// https://github.com/ScopeLift/stealth-address-erc-contracts/blob/main/src/interfaces/IERC5564Announcer.sol
|
|
2
|
+
// SPDX-License-Identifier: CC0-1.0
|
|
3
|
+
pragma solidity ^0.8.34;
|
|
4
|
+
|
|
5
|
+
/// @notice Interface for calling the `ERC5564Announcer` contract, which emits an `Announcement`
|
|
6
|
+
/// event to broadcast information about a transaction involving a stealth address. See
|
|
7
|
+
/// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) to learn more.
|
|
8
|
+
interface IERC5564Announcer {
|
|
9
|
+
/// @notice Emitted when something is sent to a stealth address.
|
|
10
|
+
/// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for
|
|
11
|
+
/// secp256k1, as specified in ERC-5564.
|
|
12
|
+
/// @param stealthAddress The computed stealth address for the recipient.
|
|
13
|
+
/// @param caller The caller of the `announce` function that emitted this event.
|
|
14
|
+
/// @param ephemeralPubKey Ephemeral public key used by the sender to derive the `stealthAddress`.
|
|
15
|
+
/// @param metadata Arbitrary data to emit with the event. The first byte MUST be the view tag.
|
|
16
|
+
/// @dev The remaining metadata can be used by the senders however they like. See
|
|
17
|
+
/// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) for recommendations on how to structure
|
|
18
|
+
/// this metadata.
|
|
19
|
+
event Announcement(
|
|
20
|
+
uint256 indexed schemeId,
|
|
21
|
+
address indexed stealthAddress,
|
|
22
|
+
address indexed caller,
|
|
23
|
+
bytes ephemeralPubKey,
|
|
24
|
+
bytes metadata
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
/// @notice Called by integrators to emit an `Announcement` event.
|
|
28
|
+
/// @param schemeId Identifier corresponding to the applied stealth address scheme, e.g. 1 for
|
|
29
|
+
/// secp256k1, as specified in ERC-5564.
|
|
30
|
+
/// @param stealthAddress The computed stealth address for the recipient.
|
|
31
|
+
/// @param ephemeralPubKey Ephemeral public key used by the sender.
|
|
32
|
+
/// @param metadata Arbitrary data to emit with the event. The first byte MUST be the view tag.
|
|
33
|
+
/// @dev The remaining metadata can be used by the senders however they like. See
|
|
34
|
+
/// [ERC-5564](https://eips.ethereum.org/EIPS/eip-5564) for recommendations on how to structure
|
|
35
|
+
/// this metadata.
|
|
36
|
+
function announce(uint256 schemeId, address stealthAddress, bytes memory ephemeralPubKey, bytes memory metadata)
|
|
37
|
+
external;
|
|
38
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.34;
|
|
3
|
+
|
|
4
|
+
/// @notice The Safe surface a governance module needs: reading the owner set, and making the Safe act
|
|
5
|
+
/// on the module's behalf.
|
|
6
|
+
/// @dev Declared here rather than imported so the production sources state exactly which Safe
|
|
7
|
+
/// functions they depend on. `operation` is Safe's `Enum.Operation`, which encodes as a uint8.
|
|
8
|
+
interface ISafeGovernance {
|
|
9
|
+
/// @notice The Safe's current owners.
|
|
10
|
+
function getOwners() external view returns (address[] memory);
|
|
11
|
+
|
|
12
|
+
/// @notice Whether `owner` is one of the Safe's owners.
|
|
13
|
+
function isOwner(address owner) external view returns (bool);
|
|
14
|
+
|
|
15
|
+
/// @notice The number of signatures the Safe currently requires.
|
|
16
|
+
function getThreshold() external view returns (uint256);
|
|
17
|
+
|
|
18
|
+
/// @notice Executes a transaction as the Safe, callable only by an enabled module.
|
|
19
|
+
/// @dev Returns `false` on an inner revert rather than bubbling it, so a caller must check.
|
|
20
|
+
function execTransactionFromModuleReturnData(address to, uint256 value, bytes memory data, uint8 operation)
|
|
21
|
+
external
|
|
22
|
+
returns (bool success, bytes memory returnData);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// @notice The Safe's owner-set functions. Only the Safe itself may call them, so a module reaches
|
|
26
|
+
/// them by having the Safe call itself.
|
|
27
|
+
interface ISafeOwnerManager {
|
|
28
|
+
/// @notice Adds an owner and sets the threshold in one call.
|
|
29
|
+
function addOwnerWithThreshold(address owner, uint256 threshold) external;
|
|
30
|
+
|
|
31
|
+
/// @notice Removes an owner and sets the threshold in one call.
|
|
32
|
+
/// @param prevOwner The owner preceding `owner` in the Safe's owner linked list.
|
|
33
|
+
function removeOwner(address prevOwner, address owner, uint256 threshold) external;
|
|
34
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.34;
|
|
3
|
+
|
|
4
|
+
import { PoseidonT3 } from "poseidon-solidity/PoseidonT3.sol";
|
|
5
|
+
import { PoseidonT4 } from "poseidon-solidity/PoseidonT4.sol";
|
|
6
|
+
|
|
7
|
+
/// @title IssuerSetHashes
|
|
8
|
+
/// @notice The approved-issuer-set leaf and internal-node formulas.
|
|
9
|
+
/// @dev The single Solidity home of these formulas, so a change to the Poseidon instantiation is
|
|
10
|
+
/// one edit here plus regenerated vectors. Their counterparts are the `hash_3` leaf and `hash_2`
|
|
11
|
+
/// internal node in `packages/circuits/src/issuer.nr`, and `issuerLeaf` in
|
|
12
|
+
/// `packages/issuer-set/src/hashes.ts`; all three must agree or every honest proof stops
|
|
13
|
+
/// verifying.
|
|
14
|
+
library IssuerSetHashes {
|
|
15
|
+
/// @notice Domain separator for a leaf commitment, matching `packages/credential`'s
|
|
16
|
+
/// `ISSUER_LEAF_DOMAIN` and the circuit's `LEAF_DOMAIN`.
|
|
17
|
+
uint256 internal constant ISSUER_LEAF_DOMAIN =
|
|
18
|
+
185404551131306766954100762329350135288498146007932686448214248919067924295;
|
|
19
|
+
|
|
20
|
+
/// @notice Commits a Baby Jubjub public key to the field element the tree stores as a leaf.
|
|
21
|
+
/// @param publicKeyX The public key's x coordinate.
|
|
22
|
+
/// @param publicKeyY The public key's y coordinate.
|
|
23
|
+
/// @return The leaf value.
|
|
24
|
+
function issuerKeyLeaf(uint256 publicKeyX, uint256 publicKeyY) internal pure returns (uint256) {
|
|
25
|
+
return PoseidonT4.hash([ISSUER_LEAF_DOMAIN, publicKeyX, publicKeyY]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// @notice Combines two child nodes into their parent.
|
|
29
|
+
/// @param left The left child.
|
|
30
|
+
/// @param right The right child.
|
|
31
|
+
/// @return The parent value.
|
|
32
|
+
function internalNode(uint256 left, uint256 right) internal pure returns (uint256) {
|
|
33
|
+
return PoseidonT3.hash([left, right]);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coordination-network/contracts",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "The coordination network's Solidity contracts: the network member registry and its governance, the identity registry, the approved-issuer set, the revocation registries and the Semaphore paymaster.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/zk-kit/coordination-network.git",
|
|
9
|
+
"directory": "packages/contracts/src"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/zk-kit/coordination-network/tree/main/packages/contracts",
|
|
12
|
+
"bugs": "https://github.com/zk-kit/coordination-network/issues",
|
|
13
|
+
"files": [
|
|
14
|
+
"*.sol",
|
|
15
|
+
"revocation",
|
|
16
|
+
"base",
|
|
17
|
+
"interfaces",
|
|
18
|
+
"libraries"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"solidity",
|
|
22
|
+
"smart-contracts",
|
|
23
|
+
"account-abstraction",
|
|
24
|
+
"paymaster",
|
|
25
|
+
"verifiable-credentials",
|
|
26
|
+
"semaphore",
|
|
27
|
+
"zero-knowledge",
|
|
28
|
+
"revocation",
|
|
29
|
+
"merkle-tree",
|
|
30
|
+
"non-membership-proof"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@account-abstraction/contracts": "^0.8.0",
|
|
37
|
+
"@openzeppelin/contracts": "^5.6.1",
|
|
38
|
+
"@semaphore-protocol/contracts": "^4.14.3",
|
|
39
|
+
"@zk-kit/lean-imt-plus.sol": "0.1.1",
|
|
40
|
+
"@zk-kit/lean-imt.sol": "^2.0.1",
|
|
41
|
+
"poseidon-solidity": "^0.0.5"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Revocation contracts
|
|
2
|
+
|
|
3
|
+
On-chain revocation registries built on [LeanIMT+](https://pse.dev/blog/lean-imt-plus-efficient-merkle-tree-for-membership-and-non-membership-proofs).
|
|
4
|
+
|
|
5
|
+
An issuer revokes a credential by publishing its revocation commitment. A holder proves their credential is still valid by proving, in zero knowledge, that their commitment is _absent_ from the registry. The verifier never learns which credential was presented, and never contacts the issuer.
|
|
6
|
+
|
|
7
|
+
## Contracts
|
|
8
|
+
|
|
9
|
+
| Contract | Role |
|
|
10
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
11
|
+
| `RevocationRegistries` | Abstract base. Owns one LeanIMT+ per registry, gates writes behind a per-registry admin, and exposes the reads a holder rebuilds the tree from. Inherit it to put revocation inside your own protocol. |
|
|
12
|
+
| `Revocation` | Deployable service on top of the base. Anyone can create a registry and become its admin, and each registry keeps a bounded history of accepted roots. |
|
|
13
|
+
| `IRevocationRegistries`, `IRevocation` | Interfaces. Integrate against these. |
|
|
14
|
+
|
|
15
|
+
The split exists so the primitive and the policy can vary independently. `RevocationRegistries` owns the data structure and the access control that every revocation service needs. `Revocation` adds one particular policy on top: open registry creation and a bounded root history. A protocol that wants different policy, such as registries created only by governance or roots accepted on a different rule, inherits the base and writes its own.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```shell
|
|
20
|
+
yarn add @coordination-network/contracts
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Integrating
|
|
24
|
+
|
|
25
|
+
```solidity
|
|
26
|
+
import { IRevocation } from "@coordination-network/contracts/revocation/interfaces/IRevocation.sol";
|
|
27
|
+
|
|
28
|
+
contract MyVerifier {
|
|
29
|
+
IRevocation public immutable revocation;
|
|
30
|
+
|
|
31
|
+
function verify(uint256 registryId, uint256 revocationRoot, bytes calldata zkProof) external view {
|
|
32
|
+
// 1. verify the zero-knowledge proof; it outputs the revocation root it was built against
|
|
33
|
+
// 2. check that root is one this registry currently accepts
|
|
34
|
+
require(revocation.isValidRoot(registryId, revocationRoot), "stale or unknown revocation root");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`isValidRoot` is the only on-chain read a privacy-preserving verification needs. The commitment never appears, which is what keeps presentations unlinkable.
|
|
40
|
+
|
|
41
|
+
### Revoking
|
|
42
|
+
|
|
43
|
+
Every mutation takes the physical index of the affected leaf's predecessor. The library validates that index in `O(1)` but does not search for it, which is what keeps a write at `O(depth)` storage writes. Compute it off chain with [`@coordination-network/revocation-registry`](https://github.com/zk-kit/coordination-network/tree/main/packages/revocation-registry):
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
const lowLeafIndex = tree.lowLeafIndexOf(revocationCommitment)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
then
|
|
50
|
+
|
|
51
|
+
```solidity
|
|
52
|
+
revocation.revoke(registryId, revocationCommitment, lowLeafIndex);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The index is only valid against the tree state the transaction executes in. If another revocation lands first the transaction reverts and the index must be recomputed.
|
|
56
|
+
|
|
57
|
+
`revokeMany` rehashes each affected internal node once for the whole batch instead of once per commitment, so a batch costs less than the same revocations sent one at a time, and the saving grows with batch size.
|
|
58
|
+
|
|
59
|
+
### The root history
|
|
60
|
+
|
|
61
|
+
A holder builds their proof against the root they last saw. A revocation landing between proof generation and verification would otherwise invalidate an honest proof, so each registry keeps accepting a superseded root for `rootDuration` seconds (one hour by default, configurable per registry).
|
|
62
|
+
|
|
63
|
+
The current root never expires; it is accepted for as long as it stays current, however long that is. When a revocation replaces it, the registry stamps that moment and the old root remains acceptable for `rootDuration` seconds from then. Every root therefore gets the same window, starting when it stops being current, which is exactly the interval in which a proof built against it can still be in flight.
|
|
64
|
+
|
|
65
|
+
This also bounds the risk the window creates. A credential revoked at some moment is absent from every earlier root and present in every later one, so the newest root that still lets it verify is the one its own revocation replaced. That root expires `rootDuration` seconds later, and older roots were superseded earlier and expire sooner. A revoked credential can therefore keep verifying for at most `rootDuration` after its revocation, whatever the registry's history.
|
|
66
|
+
|
|
67
|
+
`rootDuration` may be zero, which turns the grace period off: only the current root is accepted, and every proof in flight when a revocation lands is invalidated. That is the strictest setting, for a registry where a credential revoked seconds ago must not be presentable at all, and it costs failed presentations an honest holder cannot avoid. The comparison is strict on both ends, so a zero window rejects a root superseded in the same block rather than leaving a one-timestamp gap.
|
|
68
|
+
|
|
69
|
+
## Build
|
|
70
|
+
|
|
71
|
+
The contracts are deployed and tested inside the
|
|
72
|
+
[coordination-network](https://github.com/zk-kit/coordination-network) monorepo, in the
|
|
73
|
+
`coordination-network-contracts` Foundry project:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
yarn build:contracts
|
|
77
|
+
yarn test:contracts
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Notes
|
|
81
|
+
|
|
82
|
+
- Commitments must be non-zero and below the BN254 scalar field order. Zero is reserved for the sentinel leaf and for tombstones.
|
|
83
|
+
- There is deliberately no on-chain Merkle proof verifier. Verifying a plaintext proof would reveal the commitment, and a caller willing to do that can just call `isRevoked`, which is cheaper and answers the same question. The private path is a zero-knowledge proof verified elsewhere, whose root is checked with `isValidRoot`.
|
|
84
|
+
- Registries are isolated. One deployment serves many issuers, and an admin can only write to its own registry.
|
|
85
|
+
- Admin transfers are two-step, so a typo cannot strand a registry.
|
|
86
|
+
- `test/integration/CrossLanguageRoot.t.sol` pins the contract's roots against the TypeScript package. Any drift in the hashing or the insertion rules fails a test on both sides.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.34;
|
|
3
|
+
|
|
4
|
+
import { RevocationRegistries } from "./base/RevocationRegistries.sol";
|
|
5
|
+
import { IRevocation } from "./interfaces/IRevocation.sol";
|
|
6
|
+
|
|
7
|
+
/// @title Revocation
|
|
8
|
+
/// @notice Deployable revocation service. Anyone can create a registry and revoke credentials in it.
|
|
9
|
+
/// @dev One deployment serves many issuers: each gets its own registry, its own LeanIMT+ and its own
|
|
10
|
+
/// root history, and can only write to its own. Contracts that want revocation as part of a larger
|
|
11
|
+
/// protocol can inherit {RevocationRegistries} directly instead of using this one.
|
|
12
|
+
///
|
|
13
|
+
/// The flow this contract is one third of:
|
|
14
|
+
///
|
|
15
|
+
/// 1. an issuer revokes a credential here, publishing its revocation commitment;
|
|
16
|
+
/// 2. a holder rebuilds the registry's tree off chain and proves, in zero knowledge, that their
|
|
17
|
+
/// commitment is *not* in it, which outputs the root they proved against;
|
|
18
|
+
/// 3. a verifier checks that zero-knowledge proof and calls {isValidRoot} with the root it output.
|
|
19
|
+
///
|
|
20
|
+
/// Only step 1 and the {isValidRoot} half of step 3 happen on chain. The commitment never appears in
|
|
21
|
+
/// a verification, which is what keeps a presentation unlinkable.
|
|
22
|
+
contract Revocation is IRevocation, RevocationRegistries {
|
|
23
|
+
/// @notice The accepted-root window a registry gets unless it asks for another one.
|
|
24
|
+
/// @dev One hour comfortably covers a holder syncing the tree and presenting a credential, while
|
|
25
|
+
/// bounding how long a credential revoked a moment ago can still be presented as valid.
|
|
26
|
+
uint256 public constant DEFAULT_ROOT_DURATION = 1 hours;
|
|
27
|
+
|
|
28
|
+
/// @dev The policy and root history of each registry.
|
|
29
|
+
mapping(uint256 registryId => Registry registry) internal registries;
|
|
30
|
+
|
|
31
|
+
/// @inheritdoc IRevocation
|
|
32
|
+
uint256 public override registryCounter;
|
|
33
|
+
|
|
34
|
+
/// @inheritdoc IRevocation
|
|
35
|
+
function createRegistry() external override returns (uint256 registryId) {
|
|
36
|
+
return _createRegistryWithDuration(msg.sender, DEFAULT_ROOT_DURATION);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// @inheritdoc IRevocation
|
|
40
|
+
function createRegistry(address admin) external override returns (uint256 registryId) {
|
|
41
|
+
return _createRegistryWithDuration(admin, DEFAULT_ROOT_DURATION);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// @inheritdoc IRevocation
|
|
45
|
+
function createRegistry(address admin, uint256 rootDuration) external override returns (uint256 registryId) {
|
|
46
|
+
return _createRegistryWithDuration(admin, rootDuration);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// @inheritdoc IRevocation
|
|
50
|
+
function updateRegistryAdmin(uint256 registryId, address newAdmin) external override {
|
|
51
|
+
_updateRegistryAdmin(registryId, newAdmin);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// @inheritdoc IRevocation
|
|
55
|
+
function acceptRegistryAdmin(uint256 registryId) external override {
|
|
56
|
+
_acceptRegistryAdmin(registryId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// @inheritdoc IRevocation
|
|
60
|
+
function updateRegistryRootDuration(uint256 registryId, uint256 newRootDuration)
|
|
61
|
+
external
|
|
62
|
+
override
|
|
63
|
+
onlyRegistryAdmin(registryId)
|
|
64
|
+
{
|
|
65
|
+
uint256 oldRootDuration = registries[registryId].rootDuration;
|
|
66
|
+
|
|
67
|
+
registries[registryId].rootDuration = newRootDuration;
|
|
68
|
+
|
|
69
|
+
emit RegistryRootDurationUpdated(registryId, oldRootDuration, newRootDuration);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// @inheritdoc IRevocation
|
|
73
|
+
function revoke(uint256 registryId, uint256 revocationCommitment, uint256 lowLeafIndex) external override {
|
|
74
|
+
uint256 oldRoot = getRevocationRoot(registryId);
|
|
75
|
+
|
|
76
|
+
_revoke(registryId, revocationCommitment, lowLeafIndex);
|
|
77
|
+
|
|
78
|
+
_supersedeRoot(registryId, oldRoot);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// @inheritdoc IRevocation
|
|
82
|
+
function revokeMany(uint256 registryId, uint256[] calldata revocationCommitments, uint256[] calldata lowLeafIndices)
|
|
83
|
+
external
|
|
84
|
+
override
|
|
85
|
+
{
|
|
86
|
+
uint256 oldRoot = getRevocationRoot(registryId);
|
|
87
|
+
|
|
88
|
+
_revokeMany(registryId, revocationCommitments, lowLeafIndices);
|
|
89
|
+
|
|
90
|
+
_supersedeRoot(registryId, oldRoot);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// @inheritdoc IRevocation
|
|
94
|
+
function unrevoke(uint256 registryId, uint256 revocationCommitment, uint256 predecessorIndex) external override {
|
|
95
|
+
uint256 oldRoot = getRevocationRoot(registryId);
|
|
96
|
+
|
|
97
|
+
_unrevoke(registryId, revocationCommitment, predecessorIndex);
|
|
98
|
+
|
|
99
|
+
_supersedeRoot(registryId, oldRoot);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// @inheritdoc IRevocation
|
|
103
|
+
function updateRevocationCommitment(
|
|
104
|
+
uint256 registryId,
|
|
105
|
+
uint256 oldRevocationCommitment,
|
|
106
|
+
uint256 newRevocationCommitment,
|
|
107
|
+
uint256 oldPredecessorIndex,
|
|
108
|
+
uint256 newPredecessorIndex
|
|
109
|
+
) external override {
|
|
110
|
+
uint256 oldRoot = getRevocationRoot(registryId);
|
|
111
|
+
|
|
112
|
+
_updateRevocationCommitment(
|
|
113
|
+
registryId, oldRevocationCommitment, newRevocationCommitment, oldPredecessorIndex, newPredecessorIndex
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
_supersedeRoot(registryId, oldRoot);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// @inheritdoc IRevocation
|
|
120
|
+
function isValidRoot(uint256 registryId, uint256 revocationRoot)
|
|
121
|
+
external
|
|
122
|
+
view
|
|
123
|
+
override
|
|
124
|
+
onlyExistingRegistry(registryId)
|
|
125
|
+
returns (bool)
|
|
126
|
+
{
|
|
127
|
+
if (revocationRoot == getRevocationRoot(registryId)) return true;
|
|
128
|
+
|
|
129
|
+
uint256 supersededAt = registries[registryId].rootSupersededAt[revocationRoot];
|
|
130
|
+
|
|
131
|
+
if (supersededAt == 0) return false;
|
|
132
|
+
|
|
133
|
+
// Strict, so that a `rootDuration` of zero means exactly no grace at all rather than a
|
|
134
|
+
// one-timestamp window. A superseded root is accepted for `rootDuration` seconds, counted
|
|
135
|
+
// from the moment it stopped being current.
|
|
136
|
+
//
|
|
137
|
+
// Both sides come from the same clock, and the window is a policy measured in minutes or
|
|
138
|
+
// hours, so the seconds of drift a validator can introduce cannot meaningfully lengthen or
|
|
139
|
+
// shorten it. The worst case is a proof accepted or rejected a few seconds either side of
|
|
140
|
+
// its deadline, which is what the window exists to tolerate in the first place.
|
|
141
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
142
|
+
return block.timestamp < supersededAt + registries[registryId].rootDuration;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// @inheritdoc IRevocation
|
|
146
|
+
function getRootSupersededAt(uint256 registryId, uint256 revocationRoot) external view override returns (uint256) {
|
|
147
|
+
return registries[registryId].rootSupersededAt[revocationRoot];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// @inheritdoc IRevocation
|
|
151
|
+
function getRegistryRootDuration(uint256 registryId) external view override returns (uint256) {
|
|
152
|
+
return registries[registryId].rootDuration;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// @dev Allocates the next registry id and records its root window.
|
|
156
|
+
/// @param admin The address allowed to revoke in the registry.
|
|
157
|
+
/// @param rootDuration How long a superseded root stays acceptable, in seconds.
|
|
158
|
+
/// @return registryId The id of the new registry.
|
|
159
|
+
function _createRegistryWithDuration(address admin, uint256 rootDuration) private returns (uint256 registryId) {
|
|
160
|
+
registryId = registryCounter++;
|
|
161
|
+
|
|
162
|
+
_createRegistry(registryId, admin);
|
|
163
|
+
|
|
164
|
+
registries[registryId].rootDuration = rootDuration;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// @dev Starts the expiry clock of the root a mutation just replaced.
|
|
168
|
+
///
|
|
169
|
+
/// A root's window runs from the moment it stops being current, which is the moment recorded
|
|
170
|
+
/// here. Until then it is the current root and never expires, however long it stays current.
|
|
171
|
+
/// Every root therefore gets the same full window, covering exactly the interval in which a
|
|
172
|
+
/// proof built against it can still be in flight.
|
|
173
|
+
///
|
|
174
|
+
/// This is also what bounds the exposure: the newest root that still verifies an already
|
|
175
|
+
/// revoked credential is the one that credential's own revocation replaced, so it stops being
|
|
176
|
+
/// accepted `rootDuration` seconds after that revocation.
|
|
177
|
+
///
|
|
178
|
+
/// The empty-tree root is skipped: it is not a root anybody can build a proof against.
|
|
179
|
+
///
|
|
180
|
+
/// @param registryId The id of the registry.
|
|
181
|
+
/// @param oldRoot The root the mutation replaced.
|
|
182
|
+
function _supersedeRoot(uint256 registryId, uint256 oldRoot) private {
|
|
183
|
+
if (oldRoot == 0) return;
|
|
184
|
+
|
|
185
|
+
registries[registryId].rootSupersededAt[oldRoot] = block.timestamp;
|
|
186
|
+
}
|
|
187
|
+
}
|