@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,630 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.34;
|
|
3
|
+
|
|
4
|
+
import { SNARK_SCALAR_FIELD } from "@zk-kit/lean-imt.sol/Constants.sol";
|
|
5
|
+
import { IssuerSetHashes } from "./libraries/IssuerSetHashes.sol";
|
|
6
|
+
import { IApprovedIssuerSet } from "./interfaces/IApprovedIssuerSet.sol";
|
|
7
|
+
import { NetworkMemberRegistry } from "./NetworkMemberRegistry.sol";
|
|
8
|
+
|
|
9
|
+
/// @title ApprovedIssuerSet
|
|
10
|
+
/// @notice Accumulator over approved network members' credential signing keys.
|
|
11
|
+
/// @dev A verifier reads only the root, and the circuit proves in zero knowledge that the key which
|
|
12
|
+
/// signed a credential is in the set. That is what lets a verifier learn an approved member issued a
|
|
13
|
+
/// credential without learning which one, so this contract deliberately offers no way to answer
|
|
14
|
+
/// "whose key signed this" as part of a verification.
|
|
15
|
+
///
|
|
16
|
+
/// The tree is a LeanIMT: `nodes[level][index]` holds every node this contract has ever written,
|
|
17
|
+
/// level 0 the leaves and level `depth()` the current root. Pairing is keyed on level width, never on
|
|
18
|
+
/// zero-ness — the last node of an odd-width level promotes to its parent unchanged instead of hashing
|
|
19
|
+
/// against a zero pad. {MAX_DEPTH} only bounds capacity; the live root's level tracks
|
|
20
|
+
/// `ceil(log2(leafCount))`.
|
|
21
|
+
///
|
|
22
|
+
/// `leafCount` never decreases, so a pairing never reverts to a promotion, and every node a write
|
|
23
|
+
/// changes is an ancestor of a leaf slot the write touched. That is what keeps writes witness-free:
|
|
24
|
+
/// {_recomputePaths} only ever walks up the dirty leaves' own ancestors, never a full tree or a zero
|
|
25
|
+
/// cascade.
|
|
26
|
+
contract ApprovedIssuerSet is IApprovedIssuerSet {
|
|
27
|
+
/// @notice The tree's maximum depth. A capacity bound only — the live root's actual depth is
|
|
28
|
+
/// {depth}.
|
|
29
|
+
uint256 public constant MAX_DEPTH = 20;
|
|
30
|
+
|
|
31
|
+
/// @notice The tree's leaf capacity, `2^MAX_DEPTH`.
|
|
32
|
+
uint256 public constant MAX_LEAVES = 2 ** MAX_DEPTH;
|
|
33
|
+
|
|
34
|
+
/// @notice The accepted-root window the set gets unless governance changes it.
|
|
35
|
+
uint256 public constant DEFAULT_ROOT_DURATION = 1 hours;
|
|
36
|
+
|
|
37
|
+
/// @notice How many live signing keys one member may hold.
|
|
38
|
+
/// @dev Bounds how many retirements {retireAllIssuerKeys} performs in one call, so a member
|
|
39
|
+
/// cannot make its own removal unaffordable by publishing without limit. Removal cost here is
|
|
40
|
+
/// `MAX_ISSUER_KEYS_PER_MEMBER` batched tree writes, not a function of the set's total size —
|
|
41
|
+
/// unlike the predecessor-scanning tree this contract replaced, this cap is the whole removal-cost
|
|
42
|
+
/// story.
|
|
43
|
+
uint256 public constant MAX_ISSUER_KEYS_PER_MEMBER = 8;
|
|
44
|
+
|
|
45
|
+
/// @dev Baby Jubjub in twisted Edwards form: `a·x² + y² = 1 + d·x²·y²` over the BN254 scalar field.
|
|
46
|
+
uint256 private constant CURVE_A = 168_700;
|
|
47
|
+
uint256 private constant CURVE_D = 168_696;
|
|
48
|
+
|
|
49
|
+
/// @dev The x coordinates of Baby Jubjub's eight low-order points. The curve has cofactor 8, and
|
|
50
|
+
/// for each x below every y satisfying the curve equation gives a low-order point: two for x = 0,
|
|
51
|
+
/// two each for the order-8 pair, and the single y = 0 for the other two. So rejecting these five
|
|
52
|
+
/// x coordinates rejects exactly those eight points and no key of full order.
|
|
53
|
+
uint256 private constant LOW_ORDER_X_0 = 0;
|
|
54
|
+
uint256 private constant LOW_ORDER_X_1 =
|
|
55
|
+
2_957_874_849_018_779_266_517_920_829_765_869_116_077_630_550_401_372_566_248_359_756_137_677_864_698;
|
|
56
|
+
uint256 private constant LOW_ORDER_X_2 =
|
|
57
|
+
4_342_719_913_949_491_028_786_768_530_115_087_822_524_712_248_835_451_589_697_801_404_893_164_183_326;
|
|
58
|
+
uint256 private constant LOW_ORDER_X_3 =
|
|
59
|
+
17_545_522_957_889_784_193_459_637_215_142_187_266_023_652_151_580_582_754_000_402_781_682_644_312_291;
|
|
60
|
+
uint256 private constant LOW_ORDER_X_4 =
|
|
61
|
+
18_930_368_022_820_495_955_728_484_915_491_405_972_470_733_850_014_661_777_449_844_430_438_130_630_919;
|
|
62
|
+
|
|
63
|
+
/// @inheritdoc IApprovedIssuerSet
|
|
64
|
+
address public immutable override memberRegistry;
|
|
65
|
+
|
|
66
|
+
/// @dev The accumulator. `nodes[0]` are leaves, `nodes[depth()][0]` is the root. A leaf slot with
|
|
67
|
+
/// no entry reads 0, meaning "never written" or "retired". Above level 0, 0 means "never written"
|
|
68
|
+
/// or a promoted tombstone: retiring a promoted leaf zeros every promoted copy above it, and the
|
|
69
|
+
/// copy where that chain pairs is a live zero sibling. Pairing is keyed on level width, never on a
|
|
70
|
+
/// node being zero.
|
|
71
|
+
mapping(uint256 level => mapping(uint256 index => uint256 node)) internal nodes;
|
|
72
|
+
|
|
73
|
+
/// @notice The number of leaf slots ever written, including retired ones. Never decreases.
|
|
74
|
+
/// @dev A retirement zeros a leaf's tree slot but leaves its index allocated, so a physical index
|
|
75
|
+
/// is never reused. `leafCount` is therefore both the next free index and the upper bound for
|
|
76
|
+
/// {getLeaves}.
|
|
77
|
+
uint256 public leafCount;
|
|
78
|
+
|
|
79
|
+
/// @notice The number of currently live (non-retired) keys in the set.
|
|
80
|
+
uint256 public liveCount;
|
|
81
|
+
|
|
82
|
+
/// @notice A leaf's physical slot, plus one so that 0 means "not currently live". Cleared on
|
|
83
|
+
/// retirement, which is what lets a retirement find its slot and reject a double-retire without a
|
|
84
|
+
/// witness.
|
|
85
|
+
mapping(uint256 leaf => uint256 indexPlusOne) internal leafSlot;
|
|
86
|
+
|
|
87
|
+
/// @dev The live keys of each member, so a removal can find them without an off-chain index. Order
|
|
88
|
+
/// is not preserved: retirement removes an entry by swap-pop.
|
|
89
|
+
mapping(bytes32 memberNetworkId => uint256[] leaves) internal issuerKeys;
|
|
90
|
+
|
|
91
|
+
/// @notice When a root stopped being current. Zero for a root that never was, or for the current
|
|
92
|
+
/// root — see {_claimRoot}.
|
|
93
|
+
mapping(uint256 issuerSetRoot => uint256 supersededAt) public rootSupersededAt;
|
|
94
|
+
|
|
95
|
+
/// @notice How long a superseded root stays acceptable, in seconds.
|
|
96
|
+
uint256 public rootDuration;
|
|
97
|
+
|
|
98
|
+
/// @notice The instant at or before which no superseded root is ever acceptable again.
|
|
99
|
+
/// @dev Bumped by {retireAllIssuerKeys} on every removal of a member that ever held a key, so
|
|
100
|
+
/// removing one member closes off every root superseded in the window before removal, not just the
|
|
101
|
+
/// one root the removal directly replaces.
|
|
102
|
+
uint256 public rootsInvalidatedAt;
|
|
103
|
+
|
|
104
|
+
/// @dev Whether a member has ever published a live key. Set once, never cleared, so
|
|
105
|
+
/// {retireAllIssuerKeys} can tell a member that genuinely never held signing power from one whose
|
|
106
|
+
/// keys were all retired before removal — only the latter's removal needs to invalidate history.
|
|
107
|
+
mapping(bytes32 memberNetworkId => bool) internal hasEverPublished;
|
|
108
|
+
|
|
109
|
+
event IssuerKeyPublished(bytes32 indexed memberNetworkId, uint256 leaf, uint256 index, uint256 issuerSetRoot);
|
|
110
|
+
event IssuerKeyRetired(bytes32 indexed memberNetworkId, uint256 leaf, uint256 issuerSetRoot);
|
|
111
|
+
event RootDurationUpdated(uint256 oldRootDuration, uint256 newRootDuration);
|
|
112
|
+
|
|
113
|
+
error UnauthorizedCaller();
|
|
114
|
+
error NotApproved(bytes32 memberNetworkId);
|
|
115
|
+
error CallerIsNotTheMemberRegistry();
|
|
116
|
+
error ZeroMemberRegistry();
|
|
117
|
+
error TooManyIssuerKeys(bytes32 memberNetworkId);
|
|
118
|
+
error IssuerSetFull();
|
|
119
|
+
error LeafCannotBeZero();
|
|
120
|
+
error IssuerKeyAlreadyPublished(uint256 leaf);
|
|
121
|
+
error IssuerKeyNotPublishedByMember(bytes32 memberNetworkId, uint256 leaf);
|
|
122
|
+
error PublicKeyNotInField();
|
|
123
|
+
error PublicKeyNotOnCurve();
|
|
124
|
+
error PublicKeyHasLowOrder();
|
|
125
|
+
error LeafIndexOutOfRange(uint256 index);
|
|
126
|
+
|
|
127
|
+
/// @dev Restricts a write to the member's current auth address for rotation, or governance for
|
|
128
|
+
/// recovery, mirroring how the registry gates a member's own record.
|
|
129
|
+
/// @param memberNetworkId The member whose keys are being written.
|
|
130
|
+
modifier onlyAuthAddressOrGovernance(bytes32 memberNetworkId) {
|
|
131
|
+
_onlyAuthAddressOrGovernance(memberNetworkId);
|
|
132
|
+
_;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function _onlyAuthAddressOrGovernance(bytes32 memberNetworkId) internal view {
|
|
136
|
+
NetworkMemberRegistry registry = NetworkMemberRegistry(memberRegistry);
|
|
137
|
+
address authAddress = registry.networkMemberAuthAddress(memberNetworkId);
|
|
138
|
+
|
|
139
|
+
if (msg.sender != authAddress && msg.sender != registry.owner()) revert UnauthorizedCaller();
|
|
140
|
+
if (authAddress == address(0)) revert NotApproved(memberNetworkId);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// @param memberRegistry_ The NetworkMemberRegistry this set resolves authorization against.
|
|
144
|
+
constructor(address memberRegistry_) {
|
|
145
|
+
if (memberRegistry_ == address(0)) revert ZeroMemberRegistry();
|
|
146
|
+
|
|
147
|
+
memberRegistry = memberRegistry_;
|
|
148
|
+
rootDuration = DEFAULT_ROOT_DURATION;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// @notice Publishes a member's signing key into the set.
|
|
152
|
+
/// @dev Superseded keys stay in the set, so a rotation does not invalidate credentials the old key
|
|
153
|
+
/// already signed. Retire the old key explicitly to end its life.
|
|
154
|
+
/// @param memberNetworkId The publishing member.
|
|
155
|
+
/// @param publicKeyX The Baby Jubjub public key's x coordinate.
|
|
156
|
+
/// @param publicKeyY The Baby Jubjub public key's y coordinate.
|
|
157
|
+
/// @return leaf The commitment inserted into the set.
|
|
158
|
+
function publishIssuerKey(bytes32 memberNetworkId, uint256 publicKeyX, uint256 publicKeyY)
|
|
159
|
+
external
|
|
160
|
+
onlyAuthAddressOrGovernance(memberNetworkId)
|
|
161
|
+
returns (uint256 leaf)
|
|
162
|
+
{
|
|
163
|
+
uint256[] storage memberKeys = issuerKeys[memberNetworkId];
|
|
164
|
+
if (memberKeys.length >= MAX_ISSUER_KEYS_PER_MEMBER) revert TooManyIssuerKeys(memberNetworkId);
|
|
165
|
+
|
|
166
|
+
_assertOnCurve(publicKeyX, publicKeyY);
|
|
167
|
+
|
|
168
|
+
leaf = IssuerSetHashes.issuerKeyLeaf(publicKeyX, publicKeyY);
|
|
169
|
+
// Unreachable for a genuine Poseidon output; guards the invariant that 0 means "empty or
|
|
170
|
+
// retired" everywhere else in this contract.
|
|
171
|
+
if (leaf == 0) revert LeafCannotBeZero();
|
|
172
|
+
if (leafSlot[leaf] != 0) revert IssuerKeyAlreadyPublished(leaf);
|
|
173
|
+
if (leafCount == MAX_LEAVES) revert IssuerSetFull();
|
|
174
|
+
|
|
175
|
+
uint256 index = leafCount;
|
|
176
|
+
uint256 oldRoot = nodes[_depthFor(leafCount)][0];
|
|
177
|
+
|
|
178
|
+
nodes[0][index] = leaf;
|
|
179
|
+
leafCount = index + 1;
|
|
180
|
+
++liveCount;
|
|
181
|
+
leafSlot[leaf] = index + 1;
|
|
182
|
+
memberKeys.push(leaf);
|
|
183
|
+
hasEverPublished[memberNetworkId] = true;
|
|
184
|
+
|
|
185
|
+
_recomputePath(index);
|
|
186
|
+
|
|
187
|
+
uint256 newRoot = nodes[_depthFor(leafCount)][0];
|
|
188
|
+
_supersedeRoot(oldRoot);
|
|
189
|
+
_claimRoot(newRoot);
|
|
190
|
+
|
|
191
|
+
emit IssuerKeyPublished(memberNetworkId, leaf, index, newRoot);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// @notice Publishes several of a member's signing keys in one call.
|
|
195
|
+
/// @dev All keys are validated before any tree write, and they share one {_recomputePaths} pass
|
|
196
|
+
/// instead of one per key. Reverts on the first invalid or duplicate key, so a batch either
|
|
197
|
+
/// publishes in full or not at all.
|
|
198
|
+
/// @param memberNetworkId The publishing member.
|
|
199
|
+
/// @param publicKeys The Baby Jubjub public keys to publish, each `[x, y]`.
|
|
200
|
+
/// @return leaves The commitments inserted into the set, in the same order as `publicKeys`.
|
|
201
|
+
function publishIssuerKeys(bytes32 memberNetworkId, uint256[2][] calldata publicKeys)
|
|
202
|
+
external
|
|
203
|
+
onlyAuthAddressOrGovernance(memberNetworkId)
|
|
204
|
+
returns (uint256[] memory leaves)
|
|
205
|
+
{
|
|
206
|
+
uint256 count = publicKeys.length;
|
|
207
|
+
uint256[] storage memberKeys = issuerKeys[memberNetworkId];
|
|
208
|
+
if (memberKeys.length + count > MAX_ISSUER_KEYS_PER_MEMBER) revert TooManyIssuerKeys(memberNetworkId);
|
|
209
|
+
|
|
210
|
+
uint256 startIndex = leafCount;
|
|
211
|
+
if (startIndex + count > MAX_LEAVES) revert IssuerSetFull();
|
|
212
|
+
|
|
213
|
+
// Read before the loop below writes nodes[0][startIndex]: on an empty set the depth-0 root
|
|
214
|
+
// slot is that first leaf, so reading after the loop would stamp the leaf as a bogus old root.
|
|
215
|
+
uint256 oldRoot = nodes[_depthFor(leafCount)][0];
|
|
216
|
+
|
|
217
|
+
leaves = new uint256[](count);
|
|
218
|
+
uint256[] memory dirty = new uint256[](count);
|
|
219
|
+
|
|
220
|
+
for (uint256 i; i < count; ++i) {
|
|
221
|
+
uint256 publicKeyX = publicKeys[i][0];
|
|
222
|
+
uint256 publicKeyY = publicKeys[i][1];
|
|
223
|
+
_assertOnCurve(publicKeyX, publicKeyY);
|
|
224
|
+
|
|
225
|
+
uint256 leaf = IssuerSetHashes.issuerKeyLeaf(publicKeyX, publicKeyY);
|
|
226
|
+
if (leaf == 0) revert LeafCannotBeZero();
|
|
227
|
+
if (leafSlot[leaf] != 0) revert IssuerKeyAlreadyPublished(leaf);
|
|
228
|
+
|
|
229
|
+
uint256 index = startIndex + i;
|
|
230
|
+
nodes[0][index] = leaf;
|
|
231
|
+
leafSlot[leaf] = index + 1;
|
|
232
|
+
memberKeys.push(leaf);
|
|
233
|
+
|
|
234
|
+
leaves[i] = leaf;
|
|
235
|
+
// startIndex + i is already ascending and duplicate-free, exactly what
|
|
236
|
+
// {_recomputePaths} requires.
|
|
237
|
+
dirty[i] = index;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
leafCount = startIndex + count;
|
|
241
|
+
liveCount += count;
|
|
242
|
+
// Guarded: an empty batch published nothing, so it must not count as publishing history.
|
|
243
|
+
if (count != 0) hasEverPublished[memberNetworkId] = true;
|
|
244
|
+
|
|
245
|
+
_recomputePaths(dirty, count);
|
|
246
|
+
|
|
247
|
+
uint256 newRoot = nodes[_depthFor(leafCount)][0];
|
|
248
|
+
_supersedeRoot(oldRoot);
|
|
249
|
+
_claimRoot(newRoot);
|
|
250
|
+
|
|
251
|
+
for (uint256 i; i < count; ++i) {
|
|
252
|
+
emit IssuerKeyPublished(memberNetworkId, leaves[i], startIndex + i, newRoot);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// @notice Retires one of a member's signing keys, ending its ability to verify.
|
|
257
|
+
/// @param memberNetworkId The member the key belongs to.
|
|
258
|
+
/// @param leaf The commitment to retire.
|
|
259
|
+
function retireIssuerKey(bytes32 memberNetworkId, uint256 leaf)
|
|
260
|
+
external
|
|
261
|
+
onlyAuthAddressOrGovernance(memberNetworkId)
|
|
262
|
+
{
|
|
263
|
+
_detachFromMember(memberNetworkId, leaf);
|
|
264
|
+
|
|
265
|
+
// `_detachFromMember` already proved this leaf was live and this member's, so its slot exists.
|
|
266
|
+
uint256 index = leafSlot[leaf] - 1;
|
|
267
|
+
uint256 oldRoot = nodes[_depthFor(leafCount)][0];
|
|
268
|
+
|
|
269
|
+
nodes[0][index] = 0;
|
|
270
|
+
delete leafSlot[leaf];
|
|
271
|
+
--liveCount;
|
|
272
|
+
|
|
273
|
+
_recomputePath(index);
|
|
274
|
+
|
|
275
|
+
uint256 newRoot = nodes[_depthFor(leafCount)][0];
|
|
276
|
+
_supersedeRoot(oldRoot);
|
|
277
|
+
_claimRoot(newRoot);
|
|
278
|
+
|
|
279
|
+
emit IssuerKeyRetired(memberNetworkId, leaf, newRoot);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// @inheritdoc IApprovedIssuerSet
|
|
283
|
+
/// @dev Callable only by the registry, which invokes it while removing a member so that removal
|
|
284
|
+
/// and the loss of signing power are one transaction. Two transactions would leave a window in
|
|
285
|
+
/// which a removed member's key still signs credentials that verify.
|
|
286
|
+
///
|
|
287
|
+
/// Superseding only the directly-replaced root is not enough: the removed member still holds the
|
|
288
|
+
/// key and the path behind every root superseded in the `rootDuration` window before this call, so
|
|
289
|
+
/// each of those would otherwise keep validating for up to a full window too, unattributably.
|
|
290
|
+
/// {rootsInvalidatedAt} closes every one of them in a single write instead of walking history: once
|
|
291
|
+
/// bumped to `block.timestamp`, {isValidIssuerSetRoot} refuses any non-current root superseded at
|
|
292
|
+
/// or before this instant — the current root's own fast path returns first and is never subject to
|
|
293
|
+
/// the watermark. Gated on {hasEverPublished} so a member that never held a key
|
|
294
|
+
/// bumps nothing — its removal stays a true no-op, and every other member's still-live grace
|
|
295
|
+
/// window survives it. The directly-replaced root's stamp is also deleted outright rather than
|
|
296
|
+
/// left to outlive the watermark, on the same belt-and-suspenders basis as {_claimRoot}.
|
|
297
|
+
/// The cost is that proofs other members had in flight against an invalidated root fail and have to
|
|
298
|
+
/// be rebuilt, which is the side of the trade a kill switch belongs on.
|
|
299
|
+
///
|
|
300
|
+
/// Leaves the tree untouched for a member with no live keys, but still bumps the watermark if that
|
|
301
|
+
/// member ever published: the registry calls this on every removal, key-bearing history or not.
|
|
302
|
+
function retireAllIssuerKeys(bytes32 memberNetworkId) external {
|
|
303
|
+
if (msg.sender != memberRegistry) revert CallerIsNotTheMemberRegistry();
|
|
304
|
+
|
|
305
|
+
if (hasEverPublished[memberNetworkId]) rootsInvalidatedAt = block.timestamp;
|
|
306
|
+
|
|
307
|
+
uint256[] storage memberKeys = issuerKeys[memberNetworkId];
|
|
308
|
+
uint256 count = memberKeys.length;
|
|
309
|
+
if (count == 0) return;
|
|
310
|
+
|
|
311
|
+
uint256 oldRoot = nodes[_depthFor(leafCount)][0];
|
|
312
|
+
|
|
313
|
+
uint256[] memory leaves = new uint256[](count);
|
|
314
|
+
uint256[] memory dirty = new uint256[](count);
|
|
315
|
+
|
|
316
|
+
for (uint256 i; i < count; ++i) {
|
|
317
|
+
uint256 leaf = memberKeys[i];
|
|
318
|
+
uint256 index = leafSlot[leaf] - 1;
|
|
319
|
+
|
|
320
|
+
leaves[i] = leaf;
|
|
321
|
+
|
|
322
|
+
// The member's key list is not index-ordered (retirement swap-pops it), so insertion-sort
|
|
323
|
+
// each index into `dirty` as it is found — cheap at up to MAX_ISSUER_KEYS_PER_MEMBER
|
|
324
|
+
// entries, and it is what lets {_recomputePaths} receive the sorted, deduplicated input it
|
|
325
|
+
// requires.
|
|
326
|
+
uint256 j = i;
|
|
327
|
+
while (j > 0 && dirty[j - 1] > index) {
|
|
328
|
+
dirty[j] = dirty[j - 1];
|
|
329
|
+
--j;
|
|
330
|
+
}
|
|
331
|
+
dirty[j] = index;
|
|
332
|
+
|
|
333
|
+
nodes[0][index] = 0;
|
|
334
|
+
delete leafSlot[leaf];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
liveCount -= count;
|
|
338
|
+
delete issuerKeys[memberNetworkId];
|
|
339
|
+
|
|
340
|
+
_recomputePaths(dirty, count);
|
|
341
|
+
|
|
342
|
+
uint256 newRoot = nodes[_depthFor(leafCount)][0];
|
|
343
|
+
delete rootSupersededAt[oldRoot];
|
|
344
|
+
_claimRoot(newRoot);
|
|
345
|
+
|
|
346
|
+
// One event per retired key, all carrying the same final root: batching means no intermediate
|
|
347
|
+
// root ever existed for a key retired mid-batch to carry instead.
|
|
348
|
+
for (uint256 i; i < count; ++i) {
|
|
349
|
+
emit IssuerKeyRetired(memberNetworkId, leaves[i], newRoot);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// @notice Sets how long a superseded root stays acceptable.
|
|
354
|
+
/// @param newRootDuration The new window, in seconds.
|
|
355
|
+
function updateRootDuration(uint256 newRootDuration) external {
|
|
356
|
+
if (msg.sender != NetworkMemberRegistry(memberRegistry).owner()) revert UnauthorizedCaller();
|
|
357
|
+
|
|
358
|
+
uint256 oldRootDuration = rootDuration;
|
|
359
|
+
|
|
360
|
+
rootDuration = newRootDuration;
|
|
361
|
+
emit RootDurationUpdated(oldRootDuration, newRootDuration);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/// @notice The current root of the set. Zero while the set has never been written, or after a
|
|
365
|
+
/// one-slot set's only key is retired — there is no sibling to hash the tombstone against.
|
|
366
|
+
function issuerSetRoot() public view returns (uint256) {
|
|
367
|
+
return nodes[_depthFor(leafCount)][0];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/// @notice Whether a verifier should accept a proof built against a root.
|
|
371
|
+
/// @param root The root the proof output.
|
|
372
|
+
function isValidIssuerSetRoot(uint256 root) external view returns (bool) {
|
|
373
|
+
if (root == 0) return false;
|
|
374
|
+
if (root == nodes[_depthFor(leafCount)][0]) return true;
|
|
375
|
+
|
|
376
|
+
uint256 supersededAt = rootSupersededAt[root];
|
|
377
|
+
|
|
378
|
+
if (supersededAt == 0) return false;
|
|
379
|
+
// A watermark bumped on a later removal outranks any stamp from before it, so a root that
|
|
380
|
+
// would otherwise still be inside its window stops validating the instant that removal lands.
|
|
381
|
+
if (supersededAt <= rootsInvalidatedAt) return false;
|
|
382
|
+
|
|
383
|
+
// Strict, so a `rootDuration` of zero means no grace at all rather than a one-timestamp
|
|
384
|
+
// window. The window runs from the moment the root stopped being current, so every root gets
|
|
385
|
+
// the same full window however long it was current.
|
|
386
|
+
// forge-lint: disable-next-line(block-timestamp)
|
|
387
|
+
return block.timestamp < supersededAt + rootDuration;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/// @notice The live signing keys of a member.
|
|
391
|
+
/// @dev Public because publication is already a public transaction and a holder has to enumerate
|
|
392
|
+
/// leaves to rebuild the tree anyway. What the design hides is which key signed a given
|
|
393
|
+
/// credential, which no read here reveals.
|
|
394
|
+
/// @param memberNetworkId The member to look up.
|
|
395
|
+
function issuerKeysOf(bytes32 memberNetworkId) external view returns (uint256[] memory) {
|
|
396
|
+
return issuerKeys[memberNetworkId];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/// @notice Whether a commitment is currently in the set.
|
|
400
|
+
/// @param leaf The commitment to check.
|
|
401
|
+
function isApprovedIssuerKey(uint256 leaf) external view returns (bool) {
|
|
402
|
+
return leafSlot[leaf] != 0;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/// @notice The leaf value at a physical index. Zero for a retired leaf or an index never written.
|
|
406
|
+
/// @param index The physical slot, in `[0, getLeafCount())`.
|
|
407
|
+
function getLeaf(uint256 index) external view returns (uint256) {
|
|
408
|
+
return nodes[0][index];
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/// @notice The leaf values in `[start, end)`.
|
|
412
|
+
/// @dev The private rebuild path for a holder: reading `[0, getLeafCount())` this way and
|
|
413
|
+
/// replaying the same level loop this contract uses reconstructs the whole tree off chain, so a
|
|
414
|
+
/// holder never has to call {siblingsOf} and reveal to its RPC provider which leaf it holds a
|
|
415
|
+
/// witness for.
|
|
416
|
+
/// @param start The inclusive start index.
|
|
417
|
+
/// @param end The exclusive end index.
|
|
418
|
+
function getLeaves(uint256 start, uint256 end) external view returns (uint256[] memory leaves) {
|
|
419
|
+
leaves = new uint256[](end - start);
|
|
420
|
+
|
|
421
|
+
for (uint256 i = start; i < end; ++i) {
|
|
422
|
+
leaves[i - start] = nodes[0][i];
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/// @notice A node at an arbitrary level and index.
|
|
427
|
+
/// @param level The level, 0 for leaves up to `depth()` for the current root; {MAX_DEPTH} bounds
|
|
428
|
+
/// it.
|
|
429
|
+
/// @param index The index within that level.
|
|
430
|
+
function getNode(uint256 level, uint256 index) external view returns (uint256) {
|
|
431
|
+
return nodes[level][index];
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/// @notice The path from `index` to the root: siblings at levels where one exists, and the packed
|
|
435
|
+
/// direction bits for exactly those levels, in the order the circuit's `binary_merkle_root`
|
|
436
|
+
/// consumes them.
|
|
437
|
+
/// @dev A convenience fast path, not the path a holder's normal traffic should use: calling this
|
|
438
|
+
/// per presentation tells the RPC provider, request after request, which physical leaf that holder
|
|
439
|
+
/// holds a witness for. {getLeaves} avoids that by letting a holder rebuild the tree, and every
|
|
440
|
+
/// sibling with it, without ever asking the chain "what is index's path".
|
|
441
|
+
///
|
|
442
|
+
/// A promoted level has no sibling and is skipped, so `siblings.length` can be less than `depth()`
|
|
443
|
+
/// and `pathIndex` is not `index` itself — it packs only the included levels' direction bits, bit
|
|
444
|
+
/// `i` set when `index` is the right child at the `i`th included level.
|
|
445
|
+
/// @param index The physical leaf index to build a path for.
|
|
446
|
+
function siblingsOf(uint256 index) external view returns (uint256[] memory siblings, uint256 pathIndex) {
|
|
447
|
+
if (index >= leafCount) revert LeafIndexOutOfRange(index);
|
|
448
|
+
|
|
449
|
+
uint256 width = leafCount;
|
|
450
|
+
uint256 pathDepth = _depthFor(width);
|
|
451
|
+
uint256[MAX_DEPTH] memory buffer;
|
|
452
|
+
uint256 count;
|
|
453
|
+
|
|
454
|
+
for (uint256 level; level < pathDepth; ++level) {
|
|
455
|
+
if ((index ^ 1) < width) {
|
|
456
|
+
buffer[count] = nodes[level][index ^ 1];
|
|
457
|
+
if (index & 1 == 1) pathIndex |= 2 ** count;
|
|
458
|
+
++count;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
index >>= 1;
|
|
462
|
+
width = (width + 1) >> 1;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
siblings = new uint256[](count);
|
|
466
|
+
for (uint256 i; i < count; ++i) {
|
|
467
|
+
siblings[i] = buffer[i];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/// @notice The number of physical leaf slots, including retired ones.
|
|
472
|
+
function getLeafCount() external view returns (uint256) {
|
|
473
|
+
return leafCount;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/// @notice The current depth of the tree: `ceil(log2(leafCount))`, 0 for 0 or 1 slots.
|
|
477
|
+
function depth() external view returns (uint256) {
|
|
478
|
+
return _depthFor(leafCount);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/// @notice The number of live keys in the set.
|
|
482
|
+
function getSize() external view returns (uint256) {
|
|
483
|
+
return liveCount;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/// @dev Removes one leaf from a member's key list by swap-pop. Reverts if the leaf is not that
|
|
487
|
+
/// member's. Leaves the tree and {leafSlot} untouched — the caller does that once it knows the
|
|
488
|
+
/// leaf really is the member's.
|
|
489
|
+
/// @param memberNetworkId The member the key should belong to.
|
|
490
|
+
/// @param leaf The commitment to detach.
|
|
491
|
+
function _detachFromMember(bytes32 memberNetworkId, uint256 leaf) private {
|
|
492
|
+
uint256[] storage memberKeys = issuerKeys[memberNetworkId];
|
|
493
|
+
uint256 keyCount = memberKeys.length;
|
|
494
|
+
uint256 position = keyCount;
|
|
495
|
+
|
|
496
|
+
for (uint256 i; i < keyCount; ++i) {
|
|
497
|
+
if (memberKeys[i] != leaf) continue;
|
|
498
|
+
|
|
499
|
+
position = i;
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (position == keyCount) revert IssuerKeyNotPublishedByMember(memberNetworkId, leaf);
|
|
504
|
+
|
|
505
|
+
memberKeys[position] = memberKeys[keyCount - 1];
|
|
506
|
+
memberKeys.pop();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/// @dev Starts the expiry clock of the root a write just replaced. The empty-tree root is skipped
|
|
510
|
+
/// because no proof can be built against it.
|
|
511
|
+
/// @param oldRoot The root the write replaced.
|
|
512
|
+
function _supersedeRoot(uint256 oldRoot) private {
|
|
513
|
+
if (oldRoot == 0) return;
|
|
514
|
+
|
|
515
|
+
rootSupersededAt[oldRoot] = block.timestamp;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/// @dev Clears any supersession stamp on a root the instant it becomes current. With LeanIMT
|
|
519
|
+
/// promotion, a root can no longer recur the way a zero-padded root once could — every slot is
|
|
520
|
+
/// written at most once and never reused — so this is now a safety net against a hash collision
|
|
521
|
+
/// rather than a fix for a known revival case. Call this on every write that moves the root.
|
|
522
|
+
/// @param newRoot The root that just became current.
|
|
523
|
+
function _claimRoot(uint256 newRoot) private {
|
|
524
|
+
delete rootSupersededAt[newRoot];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/// @dev Recomputes every node from `dirty`'s leaf indices up to the root, given that each entry's
|
|
528
|
+
/// new leaf value is already written into `nodes[0]` and `leafCount` already reflects the write.
|
|
529
|
+
///
|
|
530
|
+
/// `dirty[0..length)` must arrive sorted ascending and free of duplicates. Two dirty entries that
|
|
531
|
+
/// share a parent collapse to one recompute of it: sorted order turns that collapse into a single
|
|
532
|
+
/// compare against the previous parent instead of a set lookup. The same array carries each
|
|
533
|
+
/// level's deduplicated parent indices up to the next level, so no level allocates memory of its
|
|
534
|
+
/// own. `width` halves with each level, same as {_recomputeNode} needs to decide whether a
|
|
535
|
+
/// parent's right child exists.
|
|
536
|
+
/// @param dirty The leaf indices to recompute from. Overwritten level by level with that level's
|
|
537
|
+
/// deduplicated parent indices.
|
|
538
|
+
/// @param length The number of valid entries in `dirty`.
|
|
539
|
+
function _recomputePaths(uint256[] memory dirty, uint256 length) private {
|
|
540
|
+
uint256 width = leafCount;
|
|
541
|
+
uint256 pathDepth = _depthFor(width);
|
|
542
|
+
|
|
543
|
+
for (uint256 level; level < pathDepth; ++level) {
|
|
544
|
+
uint256 nextLength;
|
|
545
|
+
uint256 previousParent = type(uint256).max;
|
|
546
|
+
|
|
547
|
+
for (uint256 i; i < length; ++i) {
|
|
548
|
+
uint256 parent = dirty[i] >> 1;
|
|
549
|
+
if (parent == previousParent) continue;
|
|
550
|
+
|
|
551
|
+
_recomputeNode(level, parent, width);
|
|
552
|
+
|
|
553
|
+
dirty[nextLength] = parent;
|
|
554
|
+
++nextLength;
|
|
555
|
+
previousParent = parent;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
length = nextLength;
|
|
559
|
+
width = (width + 1) >> 1;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/// @dev {_recomputePaths} for the single-leaf case.
|
|
564
|
+
/// @param leafIndex The touched leaf index.
|
|
565
|
+
function _recomputePath(uint256 leafIndex) private {
|
|
566
|
+
uint256[] memory dirty = new uint256[](1);
|
|
567
|
+
dirty[0] = leafIndex;
|
|
568
|
+
_recomputePaths(dirty, 1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/// @dev Recomputes one parent from its children, promoting the left child unchanged when the
|
|
572
|
+
/// level's width leaves it unpaired — the LeanIMT rule in place of a zero-padded hash. Reads the
|
|
573
|
+
/// left child once since both branches need it, and split out of {_recomputePaths} to keep that
|
|
574
|
+
/// loop's stack frame small.
|
|
575
|
+
/// @param level The children's level.
|
|
576
|
+
/// @param parent The parent index to write, at `level + 1`.
|
|
577
|
+
/// @param width The live width of `level`, deciding whether `parent`'s right child exists.
|
|
578
|
+
function _recomputeNode(uint256 level, uint256 parent, uint256 width) private {
|
|
579
|
+
uint256 leftIndex = parent << 1;
|
|
580
|
+
uint256 rightIndex = leftIndex | 1;
|
|
581
|
+
uint256 left = nodes[level][leftIndex];
|
|
582
|
+
|
|
583
|
+
nodes[level + 1][parent] =
|
|
584
|
+
rightIndex < width ? IssuerSetHashes.internalNode(left, nodes[level][rightIndex]) : left;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/// @dev The smallest `d` with `2^d >= n`; 0 for `n` of 0 or 1.
|
|
588
|
+
/// @param n The width to size a depth for.
|
|
589
|
+
function _depthFor(uint256 n) private pure returns (uint256 d) {
|
|
590
|
+
uint256 capacity = 1;
|
|
591
|
+
while (capacity < n) {
|
|
592
|
+
capacity <<= 1;
|
|
593
|
+
++d;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/// @dev Rejects a public key that is not a Baby Jubjub point of full order.
|
|
598
|
+
///
|
|
599
|
+
/// The tree would accept any field element as a leaf, so without the curve check the set could
|
|
600
|
+
/// hold a commitment to a point that does not exist.
|
|
601
|
+
///
|
|
602
|
+
/// The order check is what stops a member handing everyone else its issuing power. Under a
|
|
603
|
+
/// low-order public key `A`, the term `h·A` in EdDSA verification takes at most eight values
|
|
604
|
+
/// whatever the message, so anyone can solve `S·B = R + h·A` for a signature that verifies over a
|
|
605
|
+
/// claim tree they chose. At the identity it is immediate: `h·A` vanishes, so any `S` with
|
|
606
|
+
/// `R = S·B` passes. A member publishing such a key would let any stranger mint credentials that
|
|
607
|
+
/// verify as issued by an approved member, and issuer anonymity means no verifier could say whose
|
|
608
|
+
/// key to retire.
|
|
609
|
+
///
|
|
610
|
+
/// @param publicKeyX The x coordinate.
|
|
611
|
+
/// @param publicKeyY The y coordinate.
|
|
612
|
+
function _assertOnCurve(uint256 publicKeyX, uint256 publicKeyY) private pure {
|
|
613
|
+
if (publicKeyX >= SNARK_SCALAR_FIELD || publicKeyY >= SNARK_SCALAR_FIELD) revert PublicKeyNotInField();
|
|
614
|
+
|
|
615
|
+
if (
|
|
616
|
+
publicKeyX == LOW_ORDER_X_0 || publicKeyX == LOW_ORDER_X_1 || publicKeyX == LOW_ORDER_X_2
|
|
617
|
+
|| publicKeyX == LOW_ORDER_X_3 || publicKeyX == LOW_ORDER_X_4
|
|
618
|
+
) revert PublicKeyHasLowOrder();
|
|
619
|
+
|
|
620
|
+
uint256 xSquared = mulmod(publicKeyX, publicKeyX, SNARK_SCALAR_FIELD);
|
|
621
|
+
uint256 ySquared = mulmod(publicKeyY, publicKeyY, SNARK_SCALAR_FIELD);
|
|
622
|
+
|
|
623
|
+
uint256 left = addmod(mulmod(CURVE_A, xSquared, SNARK_SCALAR_FIELD), ySquared, SNARK_SCALAR_FIELD);
|
|
624
|
+
uint256 right = addmod(
|
|
625
|
+
1, mulmod(CURVE_D, mulmod(xSquared, ySquared, SNARK_SCALAR_FIELD), SNARK_SCALAR_FIELD), SNARK_SCALAR_FIELD
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
if (left != right) revert PublicKeyNotOnCurve();
|
|
629
|
+
}
|
|
630
|
+
}
|