@coordination-network/issuer-set 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/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/index.cjs +287 -0
- package/dist/index.js +276 -0
- package/package.json +52 -0
- package/src/approved-issuer-set.ts +217 -0
- package/src/constants.ts +26 -0
- package/src/hashes.ts +22 -0
- package/src/index.ts +12 -0
- package/src/types.ts +52 -0
- package/src/utils.ts +65 -0
- package/types/index.d.ts +204 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ethereum Foundation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# @coordination-network/issuer-set
|
|
2
|
+
|
|
3
|
+
The approved-issuer set in TypeScript: a [LeanIMT](https://github.com/zk-kit/zk-kit) accumulator over the credential signing keys of approved network members, mirroring `ApprovedIssuerSet.sol`.
|
|
4
|
+
|
|
5
|
+
A verifier needs to know that whoever signed a credential was an approved issuer at the time. The set answers that with a single Merkle proof against a root everyone agrees on, and that proof is the witness a zero-knowledge circuit consumes to show the same thing without revealing which issuer signed.
|
|
6
|
+
|
|
7
|
+
Keys are Baby Jubjub public keys in affine form, `[x, y]`. Leaves and internal nodes are Poseidon hashes over the BN254 scalar field, domain-separated by `ISSUER_LEAF_DOMAIN`, which is what keeps this implementation, the Solidity one and the circuit in agreement.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
yarn add @coordination-network/issuer-set
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
### Building the set and proving membership
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { ApprovedIssuerSet } from "@coordination-network/issuer-set"
|
|
21
|
+
|
|
22
|
+
const set = new ApprovedIssuerSet(publicKeys)
|
|
23
|
+
|
|
24
|
+
if (!set.isApproved(issuerKey)) throw new Error("issuer is not approved")
|
|
25
|
+
|
|
26
|
+
const witness = set.generateProof(issuerKey)
|
|
27
|
+
|
|
28
|
+
// witness.root is what a verifier pins against the authority
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Order matters. The tree is position-addressable, so the same keys inserted in a different order produce a different root. When rebuilding a set that already exists, supply the keys in the order that set published them.
|
|
32
|
+
|
|
33
|
+
### Rebuilding from the authority
|
|
34
|
+
|
|
35
|
+
The on-chain set is the authoritative copy.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const set = await ApprovedIssuerSet.fromContract({
|
|
39
|
+
getLeafCount: () => contract.read.getLeafCount(),
|
|
40
|
+
getLeaves: (start, end) => contract.read.getLeaves([start, end])
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Publishing and retiring keys
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const leaf = set.publish(issuerKey) // curve-validated, unlike the constructor
|
|
48
|
+
set.publishMany(issuerKeys)
|
|
49
|
+
set.retire(issuerKey) // tombstones the slot; leafCount never shrinks
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Verifying a proof
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
set.verifyProof(witness) // against this set's current root
|
|
56
|
+
ApprovedIssuerSet.verifyProof(witness) // against the root the witness carries
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Notes
|
|
60
|
+
|
|
61
|
+
- The set must not be empty, and it is capped at `2 ** ISSUER_SET_MAX_DEPTH` (depth 20) because that is the circuit's `binary_merkle_root` capacity. The tree's actual depth is dynamic and tracks `ceil(log2(leafCount))`.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var leanImt = require('@zk-kit/lean-imt');
|
|
4
|
+
var poseidon2 = require('poseidon-lite/poseidon2');
|
|
5
|
+
var poseidon3 = require('poseidon-lite/poseidon3');
|
|
6
|
+
var babyJubjub = require('@zk-kit/baby-jubjub');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The BN254 (alt_bn128) scalar field order. Every leaf and internal-node hash,
|
|
10
|
+
* and every signing-key coordinate, is an element of this field.
|
|
11
|
+
*/
|
|
12
|
+
const SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
13
|
+
/**
|
|
14
|
+
* Baby Jubjub's cofactor. A point has low order exactly when multiplying it by
|
|
15
|
+
* this gives the identity, which is what disqualifies a signing key: see
|
|
16
|
+
* `toPublicKey`.
|
|
17
|
+
*/
|
|
18
|
+
const COFACTOR = 8n;
|
|
19
|
+
/**
|
|
20
|
+
* The tree's capacity bound: spec 03's construction and the circuit's
|
|
21
|
+
* `binary_merkle_root`. The tree's actual depth is dynamic; see
|
|
22
|
+
* `ApprovedIssuerSet.depth`.
|
|
23
|
+
*/
|
|
24
|
+
const ISSUER_SET_MAX_DEPTH = 20;
|
|
25
|
+
/**
|
|
26
|
+
* Domain separator mixed into every leaf commitment, matching
|
|
27
|
+
* `IssuerSetHashes.ISSUER_LEAF_DOMAIN` in Solidity and `LEAF_DOMAIN` in the
|
|
28
|
+
* circuit.
|
|
29
|
+
*/
|
|
30
|
+
const ISSUER_LEAF_DOMAIN = 185404551131306766954100762329350135288498146007932686448214248919067924295n;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Commits a Baby Jubjub public key to the leaf value the tree stores for it.
|
|
34
|
+
* Matches `IssuerSetHashes.issuerKeyLeaf` in Solidity (`PoseidonT4`) and the
|
|
35
|
+
* leaf hash in `packages/circuits/src/issuer.nr`; all three must agree or a
|
|
36
|
+
* proof built against one never verifies against a root from another.
|
|
37
|
+
*/
|
|
38
|
+
function issuerKeyLeaf(publicKey) {
|
|
39
|
+
return poseidon3.poseidon3([ISSUER_LEAF_DOMAIN, publicKey[0], publicKey[1]]);
|
|
40
|
+
}
|
|
41
|
+
/** Alias for the name `packages/credential`'s re-export carries forward. */
|
|
42
|
+
const issuerLeaf = issuerKeyLeaf;
|
|
43
|
+
/** Combines two child nodes into their parent. Matches `IssuerSetHashes.internalNode` (`PoseidonT3`). */
|
|
44
|
+
function internalNode(left, right) {
|
|
45
|
+
return poseidon2.poseidon2([left, right]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Coerces a value to a BN254 field element, rejecting anything outside the
|
|
50
|
+
* field.
|
|
51
|
+
*
|
|
52
|
+
* Every leaf and every hash the set produces must be a field element: outside
|
|
53
|
+
* that range two distinct values reduce to the same element, so one key's
|
|
54
|
+
* membership proof would open against another's leaf.
|
|
55
|
+
*
|
|
56
|
+
* Zero is accepted, because a tombstoned or never-written slot holds it.
|
|
57
|
+
*
|
|
58
|
+
* @param value The value, as a bigint, number or decimal string.
|
|
59
|
+
* @returns The value as a bigint.
|
|
60
|
+
*/
|
|
61
|
+
function toFieldElement(value) {
|
|
62
|
+
const element = BigInt(value);
|
|
63
|
+
if (element < 0n) {
|
|
64
|
+
throw new Error("ApprovedIssuerSet: a field element must not be negative");
|
|
65
|
+
}
|
|
66
|
+
if (element >= SNARK_SCALAR_FIELD) {
|
|
67
|
+
throw new Error("ApprovedIssuerSet: a field element must be less than the SNARK scalar field order");
|
|
68
|
+
}
|
|
69
|
+
return element;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Coerces a pair to a Baby Jubjub public key of full order, rejecting a point
|
|
73
|
+
* that is not on the curve or that lies in the small-order subgroup.
|
|
74
|
+
*
|
|
75
|
+
* The set would happily commit to any two field elements, so without the curve
|
|
76
|
+
* check a caller could publish a commitment to a point that does not exist.
|
|
77
|
+
*
|
|
78
|
+
* The order check matters more. Under a low-order public key `A` the term `h·A`
|
|
79
|
+
* in EdDSA verification takes at most eight values whatever the message, so
|
|
80
|
+
* anyone can solve `S·B = R + h·A` for a signature over a claim tree they chose;
|
|
81
|
+
* at the identity it is immediate. Publishing such a key would hand every
|
|
82
|
+
* stranger the member's issuing power. Mirrors the same rejection in
|
|
83
|
+
* `ApprovedIssuerSet.sol`.
|
|
84
|
+
*
|
|
85
|
+
* @param publicKey The signing key as `[x, y]`.
|
|
86
|
+
* @returns The key with both coordinates as bigints.
|
|
87
|
+
*/
|
|
88
|
+
function toPublicKey(publicKey) {
|
|
89
|
+
const point = [toFieldElement(publicKey[0]), toFieldElement(publicKey[1])];
|
|
90
|
+
if (!babyJubjub.inCurve(point)) {
|
|
91
|
+
throw new Error("ApprovedIssuerSet: the public key is not a point on Baby Jubjub");
|
|
92
|
+
}
|
|
93
|
+
// A point has low order exactly when eight times it is the identity, the curve's cofactor being
|
|
94
|
+
// eight. That is the same set of eight points the contract rejects by x coordinate.
|
|
95
|
+
const [x, y] = babyJubjub.mulPointEscalar(point, COFACTOR);
|
|
96
|
+
if (x === 0n && y === 1n) {
|
|
97
|
+
throw new Error("ApprovedIssuerSet: the public key has low order");
|
|
98
|
+
}
|
|
99
|
+
return point;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** How many leaves {@link ApprovedIssuerSet.fromContract} requests per `getLeaves` call. */
|
|
103
|
+
const FROM_CONTRACT_PAGE_SIZE = 1000;
|
|
104
|
+
/**
|
|
105
|
+
* The approved-issuer set: a LeanIMT accumulator, capped at depth
|
|
106
|
+
* {@link ISSUER_SET_MAX_DEPTH}, over network members' credential signing keys
|
|
107
|
+
* spec 03 defines, mirroring `ApprovedIssuerSet.sol`.
|
|
108
|
+
*
|
|
109
|
+
* A retired key's slot is tombstoned to `0` rather than removed, the same
|
|
110
|
+
* dense, append-only storage the contract keeps: `leafCount` never shrinks, so
|
|
111
|
+
* a slot's Merkle position never changes once assigned. A node with no right
|
|
112
|
+
* sibling is promoted unchanged rather than hashed against a zero, so the
|
|
113
|
+
* tree's depth tracks `ceil(log2(leafCount))` instead of staying fixed.
|
|
114
|
+
*/
|
|
115
|
+
class ApprovedIssuerSet {
|
|
116
|
+
/**
|
|
117
|
+
* @param publicKeys The set's current signing keys, in order. Not
|
|
118
|
+
* curve-validated: this bulk-load path mirrors an already-validated
|
|
119
|
+
* on-chain state, unlike {@link publish}.
|
|
120
|
+
*/
|
|
121
|
+
constructor(publicKeys) {
|
|
122
|
+
if (publicKeys.length === 0)
|
|
123
|
+
throw new Error("approved issuer set must not be empty");
|
|
124
|
+
if (publicKeys.length > 2 ** ISSUER_SET_MAX_DEPTH) {
|
|
125
|
+
throw new Error("approved issuer set exceeds circuit capacity");
|
|
126
|
+
}
|
|
127
|
+
const leaves = publicKeys.map(issuerKeyLeaf);
|
|
128
|
+
// Compared as leaves, not as coordinate pairs, so this agrees with `publish`'s own check.
|
|
129
|
+
if (new Set(leaves).size !== leaves.length) {
|
|
130
|
+
throw new Error("approved issuer set contains a duplicate key");
|
|
131
|
+
}
|
|
132
|
+
this.tree = new leanImt.LeanIMT(internalNode, leaves);
|
|
133
|
+
}
|
|
134
|
+
/** The current root. Never `undefined`: both constructors require at least one slot. */
|
|
135
|
+
get root() {
|
|
136
|
+
return this.tree.root;
|
|
137
|
+
}
|
|
138
|
+
/** The number of physical leaf slots, including tombstoned ones. Never decreases. */
|
|
139
|
+
get leafCount() {
|
|
140
|
+
return this.tree.size;
|
|
141
|
+
}
|
|
142
|
+
/** The number of currently approved (non-tombstoned) keys. */
|
|
143
|
+
get size() {
|
|
144
|
+
return this.tree.leaves.filter((leaf) => leaf !== 0n).length;
|
|
145
|
+
}
|
|
146
|
+
/** `ceil(log2(leafCount))`, 0 for a one-slot set. Grows as slots are added; never shrinks. */
|
|
147
|
+
get depth() {
|
|
148
|
+
return this.tree.depth;
|
|
149
|
+
}
|
|
150
|
+
/** Every physical slot, in order, `0` for a tombstone. A copy: mutate through {@link publish}/{@link retire}. */
|
|
151
|
+
get leafSlots() {
|
|
152
|
+
return this.tree.leaves;
|
|
153
|
+
}
|
|
154
|
+
/** The leaf a key would occupy, without publishing it. */
|
|
155
|
+
static leafOf(publicKey) {
|
|
156
|
+
return issuerKeyLeaf(toPublicKey(publicKey));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Rebuilds a set from the physical slot table a holder reads with
|
|
160
|
+
* `ApprovedIssuerSet.getLeaves` — the private path that never asks the
|
|
161
|
+
* chain which leaf it holds a witness for (see the contract's
|
|
162
|
+
* `siblingsOf` natspec).
|
|
163
|
+
*
|
|
164
|
+
* @param leaves Every slot, in physical order, `0` for a tombstone.
|
|
165
|
+
* @throws if `leaves` is empty. A drained set (every slot tombstoned) is
|
|
166
|
+
* a valid, nonzero-length input; the on-chain analogue is `leafCount`
|
|
167
|
+
* being monotonic, so a set that has ever published a key is never truly
|
|
168
|
+
* empty again.
|
|
169
|
+
*/
|
|
170
|
+
static fromLeaves(leaves) {
|
|
171
|
+
if (leaves.length === 0)
|
|
172
|
+
throw new Error("approved issuer set must not be empty");
|
|
173
|
+
// Bypasses the public constructor, which validates a list of public keys: these are already
|
|
174
|
+
// leaf hashes, not keys, mirroring a state the caller trusts (the contract's own storage).
|
|
175
|
+
const set = Object.create(ApprovedIssuerSet.prototype);
|
|
176
|
+
set.tree = new leanImt.LeanIMT(internalNode, leaves.map(toFieldElement));
|
|
177
|
+
return set;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Rebuilds a set from a deployed `ApprovedIssuerSet.sol`, paging through
|
|
181
|
+
* `getLeaves` so a large set never needs one unbounded call. Pages are
|
|
182
|
+
* fetched concurrently — the reader isn't block-pinned, so this is no
|
|
183
|
+
* weaker a read than the sequential form.
|
|
184
|
+
*/
|
|
185
|
+
static async fromContract(reader) {
|
|
186
|
+
const leafCount = Number(await reader.getLeafCount());
|
|
187
|
+
const ranges = [];
|
|
188
|
+
for (let start = 0; start < leafCount; start += FROM_CONTRACT_PAGE_SIZE) {
|
|
189
|
+
ranges.push([start, Math.min(start + FROM_CONTRACT_PAGE_SIZE, leafCount)]);
|
|
190
|
+
}
|
|
191
|
+
const pages = await Promise.all(ranges.map(([start, end]) => reader.getLeaves(start, end)));
|
|
192
|
+
return ApprovedIssuerSet.fromLeaves(pages.flat());
|
|
193
|
+
}
|
|
194
|
+
/** Whether a witness is internally consistent with the root it carries, independent of any set. */
|
|
195
|
+
static verifyProof(proof) {
|
|
196
|
+
if (proof.indices.length !== proof.siblings.length)
|
|
197
|
+
return false;
|
|
198
|
+
let packed = 0;
|
|
199
|
+
for (let level = 0; level < proof.indices.length; level += 1) {
|
|
200
|
+
if (proof.indices[level])
|
|
201
|
+
packed |= 1 << level;
|
|
202
|
+
}
|
|
203
|
+
if (packed !== proof.pathIndex)
|
|
204
|
+
return false;
|
|
205
|
+
return leanImt.LeanIMT.verifyProof({ root: proof.root, leaf: proof.leaf, index: proof.pathIndex, siblings: proof.siblings }, internalNode);
|
|
206
|
+
}
|
|
207
|
+
/** Whether a signing key is currently approved. */
|
|
208
|
+
isApproved(publicKey) {
|
|
209
|
+
return this.indexOf(publicKey) >= 0;
|
|
210
|
+
}
|
|
211
|
+
/** The physical index of a currently approved key, or `-1`. */
|
|
212
|
+
indexOf(publicKey) {
|
|
213
|
+
return this.tree.indexOf(issuerKeyLeaf(publicKey));
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Publishes a signing key, appended at the next free slot.
|
|
217
|
+
* @returns The leaf commitment inserted.
|
|
218
|
+
* @throws if the key is off-curve, low-order, or already approved.
|
|
219
|
+
*/
|
|
220
|
+
publish(publicKey) {
|
|
221
|
+
const leaf = issuerKeyLeaf(toPublicKey(publicKey));
|
|
222
|
+
if (this.tree.has(leaf)) {
|
|
223
|
+
throw new Error("approved issuer set contains a duplicate key");
|
|
224
|
+
}
|
|
225
|
+
this.tree.insert(leaf);
|
|
226
|
+
return leaf;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* {@link publish}, for several keys in order. Each is checked against the
|
|
230
|
+
* ones already in this batch too, and the set is rebuilt once at the end
|
|
231
|
+
* rather than after every key.
|
|
232
|
+
* @throws if any key is invalid or a duplicate, leaving the set unchanged.
|
|
233
|
+
*/
|
|
234
|
+
publishMany(publicKeys) {
|
|
235
|
+
if (publicKeys.length === 0)
|
|
236
|
+
return [];
|
|
237
|
+
const leaves = [];
|
|
238
|
+
for (const publicKey of publicKeys) {
|
|
239
|
+
const leaf = issuerKeyLeaf(toPublicKey(publicKey));
|
|
240
|
+
if (this.tree.has(leaf) || leaves.includes(leaf)) {
|
|
241
|
+
throw new Error("approved issuer set contains a duplicate key");
|
|
242
|
+
}
|
|
243
|
+
leaves.push(leaf);
|
|
244
|
+
}
|
|
245
|
+
this.tree.insertMany(leaves);
|
|
246
|
+
return leaves;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Tombstones a key's slot to `0`, ending its ability to verify. The slot
|
|
250
|
+
* stays allocated; a republished key gets a new one.
|
|
251
|
+
* @throws if the key is off-curve, low-order, or not currently approved.
|
|
252
|
+
*/
|
|
253
|
+
retire(publicKey) {
|
|
254
|
+
const index = this.indexOf(toPublicKey(publicKey));
|
|
255
|
+
if (index < 0)
|
|
256
|
+
throw new Error("credential issuer is not in the approved set");
|
|
257
|
+
this.tree.update(index, 0n);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Generates the membership witness the credential circuit consumes.
|
|
261
|
+
* @throws if the key is off-curve, low-order, or not currently approved.
|
|
262
|
+
*/
|
|
263
|
+
generateProof(publicKey) {
|
|
264
|
+
const leaf = issuerKeyLeaf(toPublicKey(publicKey));
|
|
265
|
+
const leafIndex = this.tree.indexOf(leaf);
|
|
266
|
+
if (leafIndex < 0)
|
|
267
|
+
throw new Error("credential issuer is not in the approved set");
|
|
268
|
+
const proof = this.tree.generateProof(leafIndex);
|
|
269
|
+
const indices = Array.from({ length: proof.siblings.length }, (_, level) => ((proof.index >> level) & 1) === 1);
|
|
270
|
+
return { root: this.root, leafIndex, pathIndex: proof.index, siblings: proof.siblings, indices, leaf };
|
|
271
|
+
}
|
|
272
|
+
/** Instance form of {@link ApprovedIssuerSet.verifyProof}, additionally bound to this set's current root. */
|
|
273
|
+
verifyProof(proof) {
|
|
274
|
+
return proof.root === this.root && ApprovedIssuerSet.verifyProof(proof);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
exports.ApprovedIssuerSet = ApprovedIssuerSet;
|
|
279
|
+
exports.COFACTOR = COFACTOR;
|
|
280
|
+
exports.ISSUER_LEAF_DOMAIN = ISSUER_LEAF_DOMAIN;
|
|
281
|
+
exports.ISSUER_SET_MAX_DEPTH = ISSUER_SET_MAX_DEPTH;
|
|
282
|
+
exports.SNARK_SCALAR_FIELD = SNARK_SCALAR_FIELD;
|
|
283
|
+
exports.internalNode = internalNode;
|
|
284
|
+
exports.issuerKeyLeaf = issuerKeyLeaf;
|
|
285
|
+
exports.issuerLeaf = issuerLeaf;
|
|
286
|
+
exports.toFieldElement = toFieldElement;
|
|
287
|
+
exports.toPublicKey = toPublicKey;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { LeanIMT } from '@zk-kit/lean-imt';
|
|
2
|
+
import { poseidon2 } from 'poseidon-lite/poseidon2';
|
|
3
|
+
import { poseidon3 } from 'poseidon-lite/poseidon3';
|
|
4
|
+
import { inCurve, mulPointEscalar } from '@zk-kit/baby-jubjub';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The BN254 (alt_bn128) scalar field order. Every leaf and internal-node hash,
|
|
8
|
+
* and every signing-key coordinate, is an element of this field.
|
|
9
|
+
*/
|
|
10
|
+
const SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
11
|
+
/**
|
|
12
|
+
* Baby Jubjub's cofactor. A point has low order exactly when multiplying it by
|
|
13
|
+
* this gives the identity, which is what disqualifies a signing key: see
|
|
14
|
+
* `toPublicKey`.
|
|
15
|
+
*/
|
|
16
|
+
const COFACTOR = 8n;
|
|
17
|
+
/**
|
|
18
|
+
* The tree's capacity bound: spec 03's construction and the circuit's
|
|
19
|
+
* `binary_merkle_root`. The tree's actual depth is dynamic; see
|
|
20
|
+
* `ApprovedIssuerSet.depth`.
|
|
21
|
+
*/
|
|
22
|
+
const ISSUER_SET_MAX_DEPTH = 20;
|
|
23
|
+
/**
|
|
24
|
+
* Domain separator mixed into every leaf commitment, matching
|
|
25
|
+
* `IssuerSetHashes.ISSUER_LEAF_DOMAIN` in Solidity and `LEAF_DOMAIN` in the
|
|
26
|
+
* circuit.
|
|
27
|
+
*/
|
|
28
|
+
const ISSUER_LEAF_DOMAIN = 185404551131306766954100762329350135288498146007932686448214248919067924295n;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Commits a Baby Jubjub public key to the leaf value the tree stores for it.
|
|
32
|
+
* Matches `IssuerSetHashes.issuerKeyLeaf` in Solidity (`PoseidonT4`) and the
|
|
33
|
+
* leaf hash in `packages/circuits/src/issuer.nr`; all three must agree or a
|
|
34
|
+
* proof built against one never verifies against a root from another.
|
|
35
|
+
*/
|
|
36
|
+
function issuerKeyLeaf(publicKey) {
|
|
37
|
+
return poseidon3([ISSUER_LEAF_DOMAIN, publicKey[0], publicKey[1]]);
|
|
38
|
+
}
|
|
39
|
+
/** Alias for the name `packages/credential`'s re-export carries forward. */
|
|
40
|
+
const issuerLeaf = issuerKeyLeaf;
|
|
41
|
+
/** Combines two child nodes into their parent. Matches `IssuerSetHashes.internalNode` (`PoseidonT3`). */
|
|
42
|
+
function internalNode(left, right) {
|
|
43
|
+
return poseidon2([left, right]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Coerces a value to a BN254 field element, rejecting anything outside the
|
|
48
|
+
* field.
|
|
49
|
+
*
|
|
50
|
+
* Every leaf and every hash the set produces must be a field element: outside
|
|
51
|
+
* that range two distinct values reduce to the same element, so one key's
|
|
52
|
+
* membership proof would open against another's leaf.
|
|
53
|
+
*
|
|
54
|
+
* Zero is accepted, because a tombstoned or never-written slot holds it.
|
|
55
|
+
*
|
|
56
|
+
* @param value The value, as a bigint, number or decimal string.
|
|
57
|
+
* @returns The value as a bigint.
|
|
58
|
+
*/
|
|
59
|
+
function toFieldElement(value) {
|
|
60
|
+
const element = BigInt(value);
|
|
61
|
+
if (element < 0n) {
|
|
62
|
+
throw new Error("ApprovedIssuerSet: a field element must not be negative");
|
|
63
|
+
}
|
|
64
|
+
if (element >= SNARK_SCALAR_FIELD) {
|
|
65
|
+
throw new Error("ApprovedIssuerSet: a field element must be less than the SNARK scalar field order");
|
|
66
|
+
}
|
|
67
|
+
return element;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Coerces a pair to a Baby Jubjub public key of full order, rejecting a point
|
|
71
|
+
* that is not on the curve or that lies in the small-order subgroup.
|
|
72
|
+
*
|
|
73
|
+
* The set would happily commit to any two field elements, so without the curve
|
|
74
|
+
* check a caller could publish a commitment to a point that does not exist.
|
|
75
|
+
*
|
|
76
|
+
* The order check matters more. Under a low-order public key `A` the term `h·A`
|
|
77
|
+
* in EdDSA verification takes at most eight values whatever the message, so
|
|
78
|
+
* anyone can solve `S·B = R + h·A` for a signature over a claim tree they chose;
|
|
79
|
+
* at the identity it is immediate. Publishing such a key would hand every
|
|
80
|
+
* stranger the member's issuing power. Mirrors the same rejection in
|
|
81
|
+
* `ApprovedIssuerSet.sol`.
|
|
82
|
+
*
|
|
83
|
+
* @param publicKey The signing key as `[x, y]`.
|
|
84
|
+
* @returns The key with both coordinates as bigints.
|
|
85
|
+
*/
|
|
86
|
+
function toPublicKey(publicKey) {
|
|
87
|
+
const point = [toFieldElement(publicKey[0]), toFieldElement(publicKey[1])];
|
|
88
|
+
if (!inCurve(point)) {
|
|
89
|
+
throw new Error("ApprovedIssuerSet: the public key is not a point on Baby Jubjub");
|
|
90
|
+
}
|
|
91
|
+
// A point has low order exactly when eight times it is the identity, the curve's cofactor being
|
|
92
|
+
// eight. That is the same set of eight points the contract rejects by x coordinate.
|
|
93
|
+
const [x, y] = mulPointEscalar(point, COFACTOR);
|
|
94
|
+
if (x === 0n && y === 1n) {
|
|
95
|
+
throw new Error("ApprovedIssuerSet: the public key has low order");
|
|
96
|
+
}
|
|
97
|
+
return point;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** How many leaves {@link ApprovedIssuerSet.fromContract} requests per `getLeaves` call. */
|
|
101
|
+
const FROM_CONTRACT_PAGE_SIZE = 1000;
|
|
102
|
+
/**
|
|
103
|
+
* The approved-issuer set: a LeanIMT accumulator, capped at depth
|
|
104
|
+
* {@link ISSUER_SET_MAX_DEPTH}, over network members' credential signing keys
|
|
105
|
+
* spec 03 defines, mirroring `ApprovedIssuerSet.sol`.
|
|
106
|
+
*
|
|
107
|
+
* A retired key's slot is tombstoned to `0` rather than removed, the same
|
|
108
|
+
* dense, append-only storage the contract keeps: `leafCount` never shrinks, so
|
|
109
|
+
* a slot's Merkle position never changes once assigned. A node with no right
|
|
110
|
+
* sibling is promoted unchanged rather than hashed against a zero, so the
|
|
111
|
+
* tree's depth tracks `ceil(log2(leafCount))` instead of staying fixed.
|
|
112
|
+
*/
|
|
113
|
+
class ApprovedIssuerSet {
|
|
114
|
+
/**
|
|
115
|
+
* @param publicKeys The set's current signing keys, in order. Not
|
|
116
|
+
* curve-validated: this bulk-load path mirrors an already-validated
|
|
117
|
+
* on-chain state, unlike {@link publish}.
|
|
118
|
+
*/
|
|
119
|
+
constructor(publicKeys) {
|
|
120
|
+
if (publicKeys.length === 0)
|
|
121
|
+
throw new Error("approved issuer set must not be empty");
|
|
122
|
+
if (publicKeys.length > 2 ** ISSUER_SET_MAX_DEPTH) {
|
|
123
|
+
throw new Error("approved issuer set exceeds circuit capacity");
|
|
124
|
+
}
|
|
125
|
+
const leaves = publicKeys.map(issuerKeyLeaf);
|
|
126
|
+
// Compared as leaves, not as coordinate pairs, so this agrees with `publish`'s own check.
|
|
127
|
+
if (new Set(leaves).size !== leaves.length) {
|
|
128
|
+
throw new Error("approved issuer set contains a duplicate key");
|
|
129
|
+
}
|
|
130
|
+
this.tree = new LeanIMT(internalNode, leaves);
|
|
131
|
+
}
|
|
132
|
+
/** The current root. Never `undefined`: both constructors require at least one slot. */
|
|
133
|
+
get root() {
|
|
134
|
+
return this.tree.root;
|
|
135
|
+
}
|
|
136
|
+
/** The number of physical leaf slots, including tombstoned ones. Never decreases. */
|
|
137
|
+
get leafCount() {
|
|
138
|
+
return this.tree.size;
|
|
139
|
+
}
|
|
140
|
+
/** The number of currently approved (non-tombstoned) keys. */
|
|
141
|
+
get size() {
|
|
142
|
+
return this.tree.leaves.filter((leaf) => leaf !== 0n).length;
|
|
143
|
+
}
|
|
144
|
+
/** `ceil(log2(leafCount))`, 0 for a one-slot set. Grows as slots are added; never shrinks. */
|
|
145
|
+
get depth() {
|
|
146
|
+
return this.tree.depth;
|
|
147
|
+
}
|
|
148
|
+
/** Every physical slot, in order, `0` for a tombstone. A copy: mutate through {@link publish}/{@link retire}. */
|
|
149
|
+
get leafSlots() {
|
|
150
|
+
return this.tree.leaves;
|
|
151
|
+
}
|
|
152
|
+
/** The leaf a key would occupy, without publishing it. */
|
|
153
|
+
static leafOf(publicKey) {
|
|
154
|
+
return issuerKeyLeaf(toPublicKey(publicKey));
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Rebuilds a set from the physical slot table a holder reads with
|
|
158
|
+
* `ApprovedIssuerSet.getLeaves` — the private path that never asks the
|
|
159
|
+
* chain which leaf it holds a witness for (see the contract's
|
|
160
|
+
* `siblingsOf` natspec).
|
|
161
|
+
*
|
|
162
|
+
* @param leaves Every slot, in physical order, `0` for a tombstone.
|
|
163
|
+
* @throws if `leaves` is empty. A drained set (every slot tombstoned) is
|
|
164
|
+
* a valid, nonzero-length input; the on-chain analogue is `leafCount`
|
|
165
|
+
* being monotonic, so a set that has ever published a key is never truly
|
|
166
|
+
* empty again.
|
|
167
|
+
*/
|
|
168
|
+
static fromLeaves(leaves) {
|
|
169
|
+
if (leaves.length === 0)
|
|
170
|
+
throw new Error("approved issuer set must not be empty");
|
|
171
|
+
// Bypasses the public constructor, which validates a list of public keys: these are already
|
|
172
|
+
// leaf hashes, not keys, mirroring a state the caller trusts (the contract's own storage).
|
|
173
|
+
const set = Object.create(ApprovedIssuerSet.prototype);
|
|
174
|
+
set.tree = new LeanIMT(internalNode, leaves.map(toFieldElement));
|
|
175
|
+
return set;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Rebuilds a set from a deployed `ApprovedIssuerSet.sol`, paging through
|
|
179
|
+
* `getLeaves` so a large set never needs one unbounded call. Pages are
|
|
180
|
+
* fetched concurrently — the reader isn't block-pinned, so this is no
|
|
181
|
+
* weaker a read than the sequential form.
|
|
182
|
+
*/
|
|
183
|
+
static async fromContract(reader) {
|
|
184
|
+
const leafCount = Number(await reader.getLeafCount());
|
|
185
|
+
const ranges = [];
|
|
186
|
+
for (let start = 0; start < leafCount; start += FROM_CONTRACT_PAGE_SIZE) {
|
|
187
|
+
ranges.push([start, Math.min(start + FROM_CONTRACT_PAGE_SIZE, leafCount)]);
|
|
188
|
+
}
|
|
189
|
+
const pages = await Promise.all(ranges.map(([start, end]) => reader.getLeaves(start, end)));
|
|
190
|
+
return ApprovedIssuerSet.fromLeaves(pages.flat());
|
|
191
|
+
}
|
|
192
|
+
/** Whether a witness is internally consistent with the root it carries, independent of any set. */
|
|
193
|
+
static verifyProof(proof) {
|
|
194
|
+
if (proof.indices.length !== proof.siblings.length)
|
|
195
|
+
return false;
|
|
196
|
+
let packed = 0;
|
|
197
|
+
for (let level = 0; level < proof.indices.length; level += 1) {
|
|
198
|
+
if (proof.indices[level])
|
|
199
|
+
packed |= 1 << level;
|
|
200
|
+
}
|
|
201
|
+
if (packed !== proof.pathIndex)
|
|
202
|
+
return false;
|
|
203
|
+
return LeanIMT.verifyProof({ root: proof.root, leaf: proof.leaf, index: proof.pathIndex, siblings: proof.siblings }, internalNode);
|
|
204
|
+
}
|
|
205
|
+
/** Whether a signing key is currently approved. */
|
|
206
|
+
isApproved(publicKey) {
|
|
207
|
+
return this.indexOf(publicKey) >= 0;
|
|
208
|
+
}
|
|
209
|
+
/** The physical index of a currently approved key, or `-1`. */
|
|
210
|
+
indexOf(publicKey) {
|
|
211
|
+
return this.tree.indexOf(issuerKeyLeaf(publicKey));
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Publishes a signing key, appended at the next free slot.
|
|
215
|
+
* @returns The leaf commitment inserted.
|
|
216
|
+
* @throws if the key is off-curve, low-order, or already approved.
|
|
217
|
+
*/
|
|
218
|
+
publish(publicKey) {
|
|
219
|
+
const leaf = issuerKeyLeaf(toPublicKey(publicKey));
|
|
220
|
+
if (this.tree.has(leaf)) {
|
|
221
|
+
throw new Error("approved issuer set contains a duplicate key");
|
|
222
|
+
}
|
|
223
|
+
this.tree.insert(leaf);
|
|
224
|
+
return leaf;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* {@link publish}, for several keys in order. Each is checked against the
|
|
228
|
+
* ones already in this batch too, and the set is rebuilt once at the end
|
|
229
|
+
* rather than after every key.
|
|
230
|
+
* @throws if any key is invalid or a duplicate, leaving the set unchanged.
|
|
231
|
+
*/
|
|
232
|
+
publishMany(publicKeys) {
|
|
233
|
+
if (publicKeys.length === 0)
|
|
234
|
+
return [];
|
|
235
|
+
const leaves = [];
|
|
236
|
+
for (const publicKey of publicKeys) {
|
|
237
|
+
const leaf = issuerKeyLeaf(toPublicKey(publicKey));
|
|
238
|
+
if (this.tree.has(leaf) || leaves.includes(leaf)) {
|
|
239
|
+
throw new Error("approved issuer set contains a duplicate key");
|
|
240
|
+
}
|
|
241
|
+
leaves.push(leaf);
|
|
242
|
+
}
|
|
243
|
+
this.tree.insertMany(leaves);
|
|
244
|
+
return leaves;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Tombstones a key's slot to `0`, ending its ability to verify. The slot
|
|
248
|
+
* stays allocated; a republished key gets a new one.
|
|
249
|
+
* @throws if the key is off-curve, low-order, or not currently approved.
|
|
250
|
+
*/
|
|
251
|
+
retire(publicKey) {
|
|
252
|
+
const index = this.indexOf(toPublicKey(publicKey));
|
|
253
|
+
if (index < 0)
|
|
254
|
+
throw new Error("credential issuer is not in the approved set");
|
|
255
|
+
this.tree.update(index, 0n);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Generates the membership witness the credential circuit consumes.
|
|
259
|
+
* @throws if the key is off-curve, low-order, or not currently approved.
|
|
260
|
+
*/
|
|
261
|
+
generateProof(publicKey) {
|
|
262
|
+
const leaf = issuerKeyLeaf(toPublicKey(publicKey));
|
|
263
|
+
const leafIndex = this.tree.indexOf(leaf);
|
|
264
|
+
if (leafIndex < 0)
|
|
265
|
+
throw new Error("credential issuer is not in the approved set");
|
|
266
|
+
const proof = this.tree.generateProof(leafIndex);
|
|
267
|
+
const indices = Array.from({ length: proof.siblings.length }, (_, level) => ((proof.index >> level) & 1) === 1);
|
|
268
|
+
return { root: this.root, leafIndex, pathIndex: proof.index, siblings: proof.siblings, indices, leaf };
|
|
269
|
+
}
|
|
270
|
+
/** Instance form of {@link ApprovedIssuerSet.verifyProof}, additionally bound to this set's current root. */
|
|
271
|
+
verifyProof(proof) {
|
|
272
|
+
return proof.root === this.root && ApprovedIssuerSet.verifyProof(proof);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export { ApprovedIssuerSet, COFACTOR, ISSUER_LEAF_DOMAIN, ISSUER_SET_MAX_DEPTH, SNARK_SCALAR_FIELD, internalNode, issuerKeyLeaf, issuerLeaf, toFieldElement, toPublicKey };
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coordination-network/issuer-set",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Approved-issuer set in TypeScript: publish network members' credential signing keys into the LeanIMT accumulator (dynamic depth, capacity 2^20) spec 03 defines, and generate the membership proofs a credential circuit consumes, as the set itself or as a mirror of one held on chain.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/zk-kit/coordination-network.git",
|
|
9
|
+
"directory": "packages/issuer-set"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/zk-kit/coordination-network/tree/main/packages/issuer-set",
|
|
12
|
+
"bugs": "https://github.com/zk-kit/coordination-network/issues",
|
|
13
|
+
"sideEffects": false,
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "dist/index.cjs",
|
|
16
|
+
"module": "dist/index.js",
|
|
17
|
+
"types": "types/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./types/index.d.ts",
|
|
21
|
+
"require": "./dist/index.cjs",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist/",
|
|
27
|
+
"types/",
|
|
28
|
+
"src/",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "rimraf dist types && rollup -c rollup.config.mjs"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@zk-kit/baby-jubjub": "1.0.3",
|
|
40
|
+
"@zk-kit/lean-imt": "2.2.5",
|
|
41
|
+
"poseidon-lite": "0.3.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
45
|
+
"rimraf": "^6.1.3",
|
|
46
|
+
"rollup": "^4.62.2",
|
|
47
|
+
"rollup-plugin-cleanup": "^3.2.1",
|
|
48
|
+
"rollup-plugin-dts": "^6.4.1",
|
|
49
|
+
"tslib": "^2.8.0",
|
|
50
|
+
"typescript": "^6.0.3"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { LeanIMT } from "@zk-kit/lean-imt"
|
|
2
|
+
import { ISSUER_SET_MAX_DEPTH } from "./constants"
|
|
3
|
+
import { internalNode, issuerKeyLeaf } from "./hashes"
|
|
4
|
+
import type { BigNumberish, ContractLeafReader, IssuerMembershipWitness, IssuerPublicKey, IssuerSet } from "./types"
|
|
5
|
+
import { toFieldElement, toPublicKey } from "./utils"
|
|
6
|
+
|
|
7
|
+
/** How many leaves {@link ApprovedIssuerSet.fromContract} requests per `getLeaves` call. */
|
|
8
|
+
const FROM_CONTRACT_PAGE_SIZE = 1000
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The approved-issuer set: a LeanIMT accumulator, capped at depth
|
|
12
|
+
* {@link ISSUER_SET_MAX_DEPTH}, over network members' credential signing keys
|
|
13
|
+
* spec 03 defines, mirroring `ApprovedIssuerSet.sol`.
|
|
14
|
+
*
|
|
15
|
+
* A retired key's slot is tombstoned to `0` rather than removed, the same
|
|
16
|
+
* dense, append-only storage the contract keeps: `leafCount` never shrinks, so
|
|
17
|
+
* a slot's Merkle position never changes once assigned. A node with no right
|
|
18
|
+
* sibling is promoted unchanged rather than hashed against a zero, so the
|
|
19
|
+
* tree's depth tracks `ceil(log2(leafCount))` instead of staying fixed.
|
|
20
|
+
*/
|
|
21
|
+
export class ApprovedIssuerSet implements IssuerSet {
|
|
22
|
+
private tree: LeanIMT<bigint>
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param publicKeys The set's current signing keys, in order. Not
|
|
26
|
+
* curve-validated: this bulk-load path mirrors an already-validated
|
|
27
|
+
* on-chain state, unlike {@link publish}.
|
|
28
|
+
*/
|
|
29
|
+
constructor(publicKeys: IssuerPublicKey[]) {
|
|
30
|
+
if (publicKeys.length === 0) throw new Error("approved issuer set must not be empty")
|
|
31
|
+
if (publicKeys.length > 2 ** ISSUER_SET_MAX_DEPTH) {
|
|
32
|
+
throw new Error("approved issuer set exceeds circuit capacity")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const leaves = publicKeys.map(issuerKeyLeaf)
|
|
36
|
+
// Compared as leaves, not as coordinate pairs, so this agrees with `publish`'s own check.
|
|
37
|
+
if (new Set(leaves).size !== leaves.length) {
|
|
38
|
+
throw new Error("approved issuer set contains a duplicate key")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
this.tree = new LeanIMT<bigint>(internalNode, leaves)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The current root. Never `undefined`: both constructors require at least one slot. */
|
|
45
|
+
get root(): bigint {
|
|
46
|
+
return this.tree.root
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The number of physical leaf slots, including tombstoned ones. Never decreases. */
|
|
50
|
+
get leafCount(): number {
|
|
51
|
+
return this.tree.size
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The number of currently approved (non-tombstoned) keys. */
|
|
55
|
+
get size(): number {
|
|
56
|
+
return this.tree.leaves.filter((leaf) => leaf !== 0n).length
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** `ceil(log2(leafCount))`, 0 for a one-slot set. Grows as slots are added; never shrinks. */
|
|
60
|
+
get depth(): number {
|
|
61
|
+
return this.tree.depth
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Every physical slot, in order, `0` for a tombstone. A copy: mutate through {@link publish}/{@link retire}. */
|
|
65
|
+
get leafSlots(): bigint[] {
|
|
66
|
+
return this.tree.leaves
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The leaf a key would occupy, without publishing it. */
|
|
70
|
+
static leafOf(publicKey: IssuerPublicKey): bigint {
|
|
71
|
+
return issuerKeyLeaf(toPublicKey(publicKey))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Rebuilds a set from the physical slot table a holder reads with
|
|
76
|
+
* `ApprovedIssuerSet.getLeaves` — the private path that never asks the
|
|
77
|
+
* chain which leaf it holds a witness for (see the contract's
|
|
78
|
+
* `siblingsOf` natspec).
|
|
79
|
+
*
|
|
80
|
+
* @param leaves Every slot, in physical order, `0` for a tombstone.
|
|
81
|
+
* @throws if `leaves` is empty. A drained set (every slot tombstoned) is
|
|
82
|
+
* a valid, nonzero-length input; the on-chain analogue is `leafCount`
|
|
83
|
+
* being monotonic, so a set that has ever published a key is never truly
|
|
84
|
+
* empty again.
|
|
85
|
+
*/
|
|
86
|
+
static fromLeaves(leaves: BigNumberish[]): ApprovedIssuerSet {
|
|
87
|
+
if (leaves.length === 0) throw new Error("approved issuer set must not be empty")
|
|
88
|
+
|
|
89
|
+
// Bypasses the public constructor, which validates a list of public keys: these are already
|
|
90
|
+
// leaf hashes, not keys, mirroring a state the caller trusts (the contract's own storage).
|
|
91
|
+
const set = Object.create(ApprovedIssuerSet.prototype) as ApprovedIssuerSet
|
|
92
|
+
|
|
93
|
+
set.tree = new LeanIMT<bigint>(internalNode, leaves.map(toFieldElement))
|
|
94
|
+
|
|
95
|
+
return set
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Rebuilds a set from a deployed `ApprovedIssuerSet.sol`, paging through
|
|
100
|
+
* `getLeaves` so a large set never needs one unbounded call. Pages are
|
|
101
|
+
* fetched concurrently — the reader isn't block-pinned, so this is no
|
|
102
|
+
* weaker a read than the sequential form.
|
|
103
|
+
*/
|
|
104
|
+
static async fromContract(reader: ContractLeafReader): Promise<ApprovedIssuerSet> {
|
|
105
|
+
const leafCount = Number(await reader.getLeafCount())
|
|
106
|
+
|
|
107
|
+
const ranges: [number, number][] = []
|
|
108
|
+
for (let start = 0; start < leafCount; start += FROM_CONTRACT_PAGE_SIZE) {
|
|
109
|
+
ranges.push([start, Math.min(start + FROM_CONTRACT_PAGE_SIZE, leafCount)])
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pages = await Promise.all(ranges.map(([start, end]) => reader.getLeaves(start, end)))
|
|
113
|
+
|
|
114
|
+
return ApprovedIssuerSet.fromLeaves(pages.flat())
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Whether a witness is internally consistent with the root it carries, independent of any set. */
|
|
118
|
+
static verifyProof(proof: IssuerMembershipWitness): boolean {
|
|
119
|
+
if (proof.indices.length !== proof.siblings.length) return false
|
|
120
|
+
|
|
121
|
+
let packed = 0
|
|
122
|
+
for (let level = 0; level < proof.indices.length; level += 1) {
|
|
123
|
+
if (proof.indices[level]) packed |= 1 << level
|
|
124
|
+
}
|
|
125
|
+
if (packed !== proof.pathIndex) return false
|
|
126
|
+
|
|
127
|
+
return LeanIMT.verifyProof(
|
|
128
|
+
{ root: proof.root, leaf: proof.leaf, index: proof.pathIndex, siblings: proof.siblings },
|
|
129
|
+
internalNode
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Whether a signing key is currently approved. */
|
|
134
|
+
isApproved(publicKey: IssuerPublicKey): boolean {
|
|
135
|
+
return this.indexOf(publicKey) >= 0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The physical index of a currently approved key, or `-1`. */
|
|
139
|
+
indexOf(publicKey: IssuerPublicKey): number {
|
|
140
|
+
return this.tree.indexOf(issuerKeyLeaf(publicKey))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Publishes a signing key, appended at the next free slot.
|
|
145
|
+
* @returns The leaf commitment inserted.
|
|
146
|
+
* @throws if the key is off-curve, low-order, or already approved.
|
|
147
|
+
*/
|
|
148
|
+
publish(publicKey: IssuerPublicKey): bigint {
|
|
149
|
+
const leaf = issuerKeyLeaf(toPublicKey(publicKey))
|
|
150
|
+
|
|
151
|
+
if (this.tree.has(leaf)) {
|
|
152
|
+
throw new Error("approved issuer set contains a duplicate key")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
this.tree.insert(leaf)
|
|
156
|
+
|
|
157
|
+
return leaf
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* {@link publish}, for several keys in order. Each is checked against the
|
|
162
|
+
* ones already in this batch too, and the set is rebuilt once at the end
|
|
163
|
+
* rather than after every key.
|
|
164
|
+
* @throws if any key is invalid or a duplicate, leaving the set unchanged.
|
|
165
|
+
*/
|
|
166
|
+
publishMany(publicKeys: IssuerPublicKey[]): bigint[] {
|
|
167
|
+
if (publicKeys.length === 0) return []
|
|
168
|
+
|
|
169
|
+
const leaves: bigint[] = []
|
|
170
|
+
|
|
171
|
+
for (const publicKey of publicKeys) {
|
|
172
|
+
const leaf = issuerKeyLeaf(toPublicKey(publicKey))
|
|
173
|
+
|
|
174
|
+
if (this.tree.has(leaf) || leaves.includes(leaf)) {
|
|
175
|
+
throw new Error("approved issuer set contains a duplicate key")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
leaves.push(leaf)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.tree.insertMany(leaves)
|
|
182
|
+
|
|
183
|
+
return leaves
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Tombstones a key's slot to `0`, ending its ability to verify. The slot
|
|
188
|
+
* stays allocated; a republished key gets a new one.
|
|
189
|
+
* @throws if the key is off-curve, low-order, or not currently approved.
|
|
190
|
+
*/
|
|
191
|
+
retire(publicKey: IssuerPublicKey): void {
|
|
192
|
+
const index = this.indexOf(toPublicKey(publicKey))
|
|
193
|
+
if (index < 0) throw new Error("credential issuer is not in the approved set")
|
|
194
|
+
|
|
195
|
+
this.tree.update(index, 0n)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Generates the membership witness the credential circuit consumes.
|
|
200
|
+
* @throws if the key is off-curve, low-order, or not currently approved.
|
|
201
|
+
*/
|
|
202
|
+
generateProof(publicKey: IssuerPublicKey): IssuerMembershipWitness {
|
|
203
|
+
const leaf = issuerKeyLeaf(toPublicKey(publicKey))
|
|
204
|
+
const leafIndex = this.tree.indexOf(leaf)
|
|
205
|
+
if (leafIndex < 0) throw new Error("credential issuer is not in the approved set")
|
|
206
|
+
|
|
207
|
+
const proof = this.tree.generateProof(leafIndex)
|
|
208
|
+
const indices = Array.from({ length: proof.siblings.length }, (_, level) => ((proof.index >> level) & 1) === 1)
|
|
209
|
+
|
|
210
|
+
return { root: this.root, leafIndex, pathIndex: proof.index, siblings: proof.siblings, indices, leaf }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Instance form of {@link ApprovedIssuerSet.verifyProof}, additionally bound to this set's current root. */
|
|
214
|
+
verifyProof(proof: IssuerMembershipWitness): boolean {
|
|
215
|
+
return proof.root === this.root && ApprovedIssuerSet.verifyProof(proof)
|
|
216
|
+
}
|
|
217
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The BN254 (alt_bn128) scalar field order. Every leaf and internal-node hash,
|
|
3
|
+
* and every signing-key coordinate, is an element of this field.
|
|
4
|
+
*/
|
|
5
|
+
export const SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Baby Jubjub's cofactor. A point has low order exactly when multiplying it by
|
|
9
|
+
* this gives the identity, which is what disqualifies a signing key: see
|
|
10
|
+
* `toPublicKey`.
|
|
11
|
+
*/
|
|
12
|
+
export const COFACTOR = 8n
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The tree's capacity bound: spec 03's construction and the circuit's
|
|
16
|
+
* `binary_merkle_root`. The tree's actual depth is dynamic; see
|
|
17
|
+
* `ApprovedIssuerSet.depth`.
|
|
18
|
+
*/
|
|
19
|
+
export const ISSUER_SET_MAX_DEPTH = 20
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Domain separator mixed into every leaf commitment, matching
|
|
23
|
+
* `IssuerSetHashes.ISSUER_LEAF_DOMAIN` in Solidity and `LEAF_DOMAIN` in the
|
|
24
|
+
* circuit.
|
|
25
|
+
*/
|
|
26
|
+
export const ISSUER_LEAF_DOMAIN = 185404551131306766954100762329350135288498146007932686448214248919067924295n
|
package/src/hashes.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { poseidon2 } from "poseidon-lite/poseidon2"
|
|
2
|
+
import { poseidon3 } from "poseidon-lite/poseidon3"
|
|
3
|
+
import { ISSUER_LEAF_DOMAIN } from "./constants"
|
|
4
|
+
import type { IssuerPublicKey } from "./types"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Commits a Baby Jubjub public key to the leaf value the tree stores for it.
|
|
8
|
+
* Matches `IssuerSetHashes.issuerKeyLeaf` in Solidity (`PoseidonT4`) and the
|
|
9
|
+
* leaf hash in `packages/circuits/src/issuer.nr`; all three must agree or a
|
|
10
|
+
* proof built against one never verifies against a root from another.
|
|
11
|
+
*/
|
|
12
|
+
export function issuerKeyLeaf(publicKey: IssuerPublicKey): bigint {
|
|
13
|
+
return poseidon3([ISSUER_LEAF_DOMAIN, publicKey[0], publicKey[1]])
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Alias for the name `packages/credential`'s re-export carries forward. */
|
|
17
|
+
export const issuerLeaf = issuerKeyLeaf
|
|
18
|
+
|
|
19
|
+
/** Combines two child nodes into their parent. Matches `IssuerSetHashes.internalNode` (`PoseidonT3`). */
|
|
20
|
+
export function internalNode(left: bigint, right: bigint): bigint {
|
|
21
|
+
return poseidon2([left, right])
|
|
22
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { ApprovedIssuerSet } from "./approved-issuer-set"
|
|
2
|
+
export { COFACTOR, ISSUER_LEAF_DOMAIN, ISSUER_SET_MAX_DEPTH, SNARK_SCALAR_FIELD } from "./constants"
|
|
3
|
+
export { internalNode, issuerKeyLeaf, issuerLeaf } from "./hashes"
|
|
4
|
+
export type {
|
|
5
|
+
BigNumberish,
|
|
6
|
+
ContractLeafReader,
|
|
7
|
+
IssuerMembershipProof,
|
|
8
|
+
IssuerMembershipWitness,
|
|
9
|
+
IssuerPublicKey,
|
|
10
|
+
IssuerSet
|
|
11
|
+
} from "./types"
|
|
12
|
+
export { toFieldElement, toPublicKey } from "./utils"
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Anything that can be coerced to a field element with `BigInt()`. */
|
|
2
|
+
export type BigNumberish = bigint | number | string
|
|
3
|
+
|
|
4
|
+
/** A Baby Jubjub public key in affine form, `[x, y]`. */
|
|
5
|
+
export type IssuerPublicKey = [bigint, bigint]
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A membership proof for one signing key: a Merkle proof of a single leaf.
|
|
9
|
+
* Structurally identical to `packages/credential`'s type of the same name, so
|
|
10
|
+
* a re-export from there type-checks unchanged. `leafIndex` is the leaf's
|
|
11
|
+
* physical slot; `pathIndex` packs the direction bits of the levels actually
|
|
12
|
+
* present in `siblings` (promoted levels have no sibling and are absent), and
|
|
13
|
+
* is what the proof opens against `root` with.
|
|
14
|
+
*/
|
|
15
|
+
export interface IssuerMembershipProof {
|
|
16
|
+
root: bigint
|
|
17
|
+
leafIndex: number
|
|
18
|
+
pathIndex: number
|
|
19
|
+
siblings: bigint[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* `ApprovedIssuerSet.generateProof`'s actual return shape: a membership proof
|
|
24
|
+
* plus the per-level direction bits and the leaf itself, together enough to
|
|
25
|
+
* fold to `root` with no other context. `indices` covers only the levels
|
|
26
|
+
* `siblings` includes — `indices[i]` is bit `i` of `pathIndex` — the circuit's
|
|
27
|
+
* `binary_merkle_root` witness.
|
|
28
|
+
*/
|
|
29
|
+
export interface IssuerMembershipWitness extends IssuerMembershipProof {
|
|
30
|
+
indices: boolean[]
|
|
31
|
+
leaf: bigint
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The structural interface `packages/credential` consumes: a root and a way
|
|
36
|
+
* to prove membership against it. `ApprovedIssuerSet` widens `generateProof`'s
|
|
37
|
+
* return type to {@link IssuerMembershipWitness}, which still satisfies this.
|
|
38
|
+
*/
|
|
39
|
+
export interface IssuerSet {
|
|
40
|
+
root: bigint
|
|
41
|
+
generateProof(publicKey: IssuerPublicKey): IssuerMembershipProof
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The minimal read surface {@link ApprovedIssuerSet.fromContract} needs from
|
|
46
|
+
* a deployed `ApprovedIssuerSet.sol`. Structural, not a viem `Contract`
|
|
47
|
+
* instance, so this package carries no on-chain dependency.
|
|
48
|
+
*/
|
|
49
|
+
export interface ContractLeafReader {
|
|
50
|
+
getLeafCount(): Promise<bigint | number>
|
|
51
|
+
getLeaves(start: number, end: number): Promise<(bigint | string | number)[]>
|
|
52
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { inCurve, mulPointEscalar } from "@zk-kit/baby-jubjub"
|
|
2
|
+
import { COFACTOR, SNARK_SCALAR_FIELD } from "./constants"
|
|
3
|
+
import type { BigNumberish, IssuerPublicKey } from "./types"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Coerces a value to a BN254 field element, rejecting anything outside the
|
|
7
|
+
* field.
|
|
8
|
+
*
|
|
9
|
+
* Every leaf and every hash the set produces must be a field element: outside
|
|
10
|
+
* that range two distinct values reduce to the same element, so one key's
|
|
11
|
+
* membership proof would open against another's leaf.
|
|
12
|
+
*
|
|
13
|
+
* Zero is accepted, because a tombstoned or never-written slot holds it.
|
|
14
|
+
*
|
|
15
|
+
* @param value The value, as a bigint, number or decimal string.
|
|
16
|
+
* @returns The value as a bigint.
|
|
17
|
+
*/
|
|
18
|
+
export function toFieldElement(value: BigNumberish): bigint {
|
|
19
|
+
const element = BigInt(value)
|
|
20
|
+
|
|
21
|
+
if (element < 0n) {
|
|
22
|
+
throw new Error("ApprovedIssuerSet: a field element must not be negative")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (element >= SNARK_SCALAR_FIELD) {
|
|
26
|
+
throw new Error("ApprovedIssuerSet: a field element must be less than the SNARK scalar field order")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return element
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Coerces a pair to a Baby Jubjub public key of full order, rejecting a point
|
|
34
|
+
* that is not on the curve or that lies in the small-order subgroup.
|
|
35
|
+
*
|
|
36
|
+
* The set would happily commit to any two field elements, so without the curve
|
|
37
|
+
* check a caller could publish a commitment to a point that does not exist.
|
|
38
|
+
*
|
|
39
|
+
* The order check matters more. Under a low-order public key `A` the term `h·A`
|
|
40
|
+
* in EdDSA verification takes at most eight values whatever the message, so
|
|
41
|
+
* anyone can solve `S·B = R + h·A` for a signature over a claim tree they chose;
|
|
42
|
+
* at the identity it is immediate. Publishing such a key would hand every
|
|
43
|
+
* stranger the member's issuing power. Mirrors the same rejection in
|
|
44
|
+
* `ApprovedIssuerSet.sol`.
|
|
45
|
+
*
|
|
46
|
+
* @param publicKey The signing key as `[x, y]`.
|
|
47
|
+
* @returns The key with both coordinates as bigints.
|
|
48
|
+
*/
|
|
49
|
+
export function toPublicKey(publicKey: IssuerPublicKey): [bigint, bigint] {
|
|
50
|
+
const point: [bigint, bigint] = [toFieldElement(publicKey[0]), toFieldElement(publicKey[1])]
|
|
51
|
+
|
|
52
|
+
if (!inCurve(point)) {
|
|
53
|
+
throw new Error("ApprovedIssuerSet: the public key is not a point on Baby Jubjub")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A point has low order exactly when eight times it is the identity, the curve's cofactor being
|
|
57
|
+
// eight. That is the same set of eight points the contract rejects by x coordinate.
|
|
58
|
+
const [x, y] = mulPointEscalar(point, COFACTOR)
|
|
59
|
+
|
|
60
|
+
if (x === 0n && y === 1n) {
|
|
61
|
+
throw new Error("ApprovedIssuerSet: the public key has low order")
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return point
|
|
65
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/** Anything that can be coerced to a field element with `BigInt()`. */
|
|
2
|
+
type BigNumberish = bigint | number | string;
|
|
3
|
+
/** A Baby Jubjub public key in affine form, `[x, y]`. */
|
|
4
|
+
type IssuerPublicKey = [bigint, bigint];
|
|
5
|
+
/**
|
|
6
|
+
* A membership proof for one signing key: a Merkle proof of a single leaf.
|
|
7
|
+
* Structurally identical to `packages/credential`'s type of the same name, so
|
|
8
|
+
* a re-export from there type-checks unchanged. `leafIndex` is the leaf's
|
|
9
|
+
* physical slot; `pathIndex` packs the direction bits of the levels actually
|
|
10
|
+
* present in `siblings` (promoted levels have no sibling and are absent), and
|
|
11
|
+
* is what the proof opens against `root` with.
|
|
12
|
+
*/
|
|
13
|
+
interface IssuerMembershipProof {
|
|
14
|
+
root: bigint;
|
|
15
|
+
leafIndex: number;
|
|
16
|
+
pathIndex: number;
|
|
17
|
+
siblings: bigint[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* `ApprovedIssuerSet.generateProof`'s actual return shape: a membership proof
|
|
21
|
+
* plus the per-level direction bits and the leaf itself, together enough to
|
|
22
|
+
* fold to `root` with no other context. `indices` covers only the levels
|
|
23
|
+
* `siblings` includes — `indices[i]` is bit `i` of `pathIndex` — the circuit's
|
|
24
|
+
* `binary_merkle_root` witness.
|
|
25
|
+
*/
|
|
26
|
+
interface IssuerMembershipWitness extends IssuerMembershipProof {
|
|
27
|
+
indices: boolean[];
|
|
28
|
+
leaf: bigint;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The structural interface `packages/credential` consumes: a root and a way
|
|
32
|
+
* to prove membership against it. `ApprovedIssuerSet` widens `generateProof`'s
|
|
33
|
+
* return type to {@link IssuerMembershipWitness}, which still satisfies this.
|
|
34
|
+
*/
|
|
35
|
+
interface IssuerSet {
|
|
36
|
+
root: bigint;
|
|
37
|
+
generateProof(publicKey: IssuerPublicKey): IssuerMembershipProof;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The minimal read surface {@link ApprovedIssuerSet.fromContract} needs from
|
|
41
|
+
* a deployed `ApprovedIssuerSet.sol`. Structural, not a viem `Contract`
|
|
42
|
+
* instance, so this package carries no on-chain dependency.
|
|
43
|
+
*/
|
|
44
|
+
interface ContractLeafReader {
|
|
45
|
+
getLeafCount(): Promise<bigint | number>;
|
|
46
|
+
getLeaves(start: number, end: number): Promise<(bigint | string | number)[]>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The approved-issuer set: a LeanIMT accumulator, capped at depth
|
|
51
|
+
* {@link ISSUER_SET_MAX_DEPTH}, over network members' credential signing keys
|
|
52
|
+
* spec 03 defines, mirroring `ApprovedIssuerSet.sol`.
|
|
53
|
+
*
|
|
54
|
+
* A retired key's slot is tombstoned to `0` rather than removed, the same
|
|
55
|
+
* dense, append-only storage the contract keeps: `leafCount` never shrinks, so
|
|
56
|
+
* a slot's Merkle position never changes once assigned. A node with no right
|
|
57
|
+
* sibling is promoted unchanged rather than hashed against a zero, so the
|
|
58
|
+
* tree's depth tracks `ceil(log2(leafCount))` instead of staying fixed.
|
|
59
|
+
*/
|
|
60
|
+
declare class ApprovedIssuerSet implements IssuerSet {
|
|
61
|
+
private tree;
|
|
62
|
+
/**
|
|
63
|
+
* @param publicKeys The set's current signing keys, in order. Not
|
|
64
|
+
* curve-validated: this bulk-load path mirrors an already-validated
|
|
65
|
+
* on-chain state, unlike {@link publish}.
|
|
66
|
+
*/
|
|
67
|
+
constructor(publicKeys: IssuerPublicKey[]);
|
|
68
|
+
/** The current root. Never `undefined`: both constructors require at least one slot. */
|
|
69
|
+
get root(): bigint;
|
|
70
|
+
/** The number of physical leaf slots, including tombstoned ones. Never decreases. */
|
|
71
|
+
get leafCount(): number;
|
|
72
|
+
/** The number of currently approved (non-tombstoned) keys. */
|
|
73
|
+
get size(): number;
|
|
74
|
+
/** `ceil(log2(leafCount))`, 0 for a one-slot set. Grows as slots are added; never shrinks. */
|
|
75
|
+
get depth(): number;
|
|
76
|
+
/** Every physical slot, in order, `0` for a tombstone. A copy: mutate through {@link publish}/{@link retire}. */
|
|
77
|
+
get leafSlots(): bigint[];
|
|
78
|
+
/** The leaf a key would occupy, without publishing it. */
|
|
79
|
+
static leafOf(publicKey: IssuerPublicKey): bigint;
|
|
80
|
+
/**
|
|
81
|
+
* Rebuilds a set from the physical slot table a holder reads with
|
|
82
|
+
* `ApprovedIssuerSet.getLeaves` — the private path that never asks the
|
|
83
|
+
* chain which leaf it holds a witness for (see the contract's
|
|
84
|
+
* `siblingsOf` natspec).
|
|
85
|
+
*
|
|
86
|
+
* @param leaves Every slot, in physical order, `0` for a tombstone.
|
|
87
|
+
* @throws if `leaves` is empty. A drained set (every slot tombstoned) is
|
|
88
|
+
* a valid, nonzero-length input; the on-chain analogue is `leafCount`
|
|
89
|
+
* being monotonic, so a set that has ever published a key is never truly
|
|
90
|
+
* empty again.
|
|
91
|
+
*/
|
|
92
|
+
static fromLeaves(leaves: BigNumberish[]): ApprovedIssuerSet;
|
|
93
|
+
/**
|
|
94
|
+
* Rebuilds a set from a deployed `ApprovedIssuerSet.sol`, paging through
|
|
95
|
+
* `getLeaves` so a large set never needs one unbounded call. Pages are
|
|
96
|
+
* fetched concurrently — the reader isn't block-pinned, so this is no
|
|
97
|
+
* weaker a read than the sequential form.
|
|
98
|
+
*/
|
|
99
|
+
static fromContract(reader: ContractLeafReader): Promise<ApprovedIssuerSet>;
|
|
100
|
+
/** Whether a witness is internally consistent with the root it carries, independent of any set. */
|
|
101
|
+
static verifyProof(proof: IssuerMembershipWitness): boolean;
|
|
102
|
+
/** Whether a signing key is currently approved. */
|
|
103
|
+
isApproved(publicKey: IssuerPublicKey): boolean;
|
|
104
|
+
/** The physical index of a currently approved key, or `-1`. */
|
|
105
|
+
indexOf(publicKey: IssuerPublicKey): number;
|
|
106
|
+
/**
|
|
107
|
+
* Publishes a signing key, appended at the next free slot.
|
|
108
|
+
* @returns The leaf commitment inserted.
|
|
109
|
+
* @throws if the key is off-curve, low-order, or already approved.
|
|
110
|
+
*/
|
|
111
|
+
publish(publicKey: IssuerPublicKey): bigint;
|
|
112
|
+
/**
|
|
113
|
+
* {@link publish}, for several keys in order. Each is checked against the
|
|
114
|
+
* ones already in this batch too, and the set is rebuilt once at the end
|
|
115
|
+
* rather than after every key.
|
|
116
|
+
* @throws if any key is invalid or a duplicate, leaving the set unchanged.
|
|
117
|
+
*/
|
|
118
|
+
publishMany(publicKeys: IssuerPublicKey[]): bigint[];
|
|
119
|
+
/**
|
|
120
|
+
* Tombstones a key's slot to `0`, ending its ability to verify. The slot
|
|
121
|
+
* stays allocated; a republished key gets a new one.
|
|
122
|
+
* @throws if the key is off-curve, low-order, or not currently approved.
|
|
123
|
+
*/
|
|
124
|
+
retire(publicKey: IssuerPublicKey): void;
|
|
125
|
+
/**
|
|
126
|
+
* Generates the membership witness the credential circuit consumes.
|
|
127
|
+
* @throws if the key is off-curve, low-order, or not currently approved.
|
|
128
|
+
*/
|
|
129
|
+
generateProof(publicKey: IssuerPublicKey): IssuerMembershipWitness;
|
|
130
|
+
/** Instance form of {@link ApprovedIssuerSet.verifyProof}, additionally bound to this set's current root. */
|
|
131
|
+
verifyProof(proof: IssuerMembershipWitness): boolean;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The BN254 (alt_bn128) scalar field order. Every leaf and internal-node hash,
|
|
136
|
+
* and every signing-key coordinate, is an element of this field.
|
|
137
|
+
*/
|
|
138
|
+
declare const SNARK_SCALAR_FIELD = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
|
|
139
|
+
/**
|
|
140
|
+
* Baby Jubjub's cofactor. A point has low order exactly when multiplying it by
|
|
141
|
+
* this gives the identity, which is what disqualifies a signing key: see
|
|
142
|
+
* `toPublicKey`.
|
|
143
|
+
*/
|
|
144
|
+
declare const COFACTOR = 8n;
|
|
145
|
+
/**
|
|
146
|
+
* The tree's capacity bound: spec 03's construction and the circuit's
|
|
147
|
+
* `binary_merkle_root`. The tree's actual depth is dynamic; see
|
|
148
|
+
* `ApprovedIssuerSet.depth`.
|
|
149
|
+
*/
|
|
150
|
+
declare const ISSUER_SET_MAX_DEPTH = 20;
|
|
151
|
+
/**
|
|
152
|
+
* Domain separator mixed into every leaf commitment, matching
|
|
153
|
+
* `IssuerSetHashes.ISSUER_LEAF_DOMAIN` in Solidity and `LEAF_DOMAIN` in the
|
|
154
|
+
* circuit.
|
|
155
|
+
*/
|
|
156
|
+
declare const ISSUER_LEAF_DOMAIN = 185404551131306766954100762329350135288498146007932686448214248919067924295n;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Commits a Baby Jubjub public key to the leaf value the tree stores for it.
|
|
160
|
+
* Matches `IssuerSetHashes.issuerKeyLeaf` in Solidity (`PoseidonT4`) and the
|
|
161
|
+
* leaf hash in `packages/circuits/src/issuer.nr`; all three must agree or a
|
|
162
|
+
* proof built against one never verifies against a root from another.
|
|
163
|
+
*/
|
|
164
|
+
declare function issuerKeyLeaf(publicKey: IssuerPublicKey): bigint;
|
|
165
|
+
/** Alias for the name `packages/credential`'s re-export carries forward. */
|
|
166
|
+
declare const issuerLeaf: typeof issuerKeyLeaf;
|
|
167
|
+
/** Combines two child nodes into their parent. Matches `IssuerSetHashes.internalNode` (`PoseidonT3`). */
|
|
168
|
+
declare function internalNode(left: bigint, right: bigint): bigint;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Coerces a value to a BN254 field element, rejecting anything outside the
|
|
172
|
+
* field.
|
|
173
|
+
*
|
|
174
|
+
* Every leaf and every hash the set produces must be a field element: outside
|
|
175
|
+
* that range two distinct values reduce to the same element, so one key's
|
|
176
|
+
* membership proof would open against another's leaf.
|
|
177
|
+
*
|
|
178
|
+
* Zero is accepted, because a tombstoned or never-written slot holds it.
|
|
179
|
+
*
|
|
180
|
+
* @param value The value, as a bigint, number or decimal string.
|
|
181
|
+
* @returns The value as a bigint.
|
|
182
|
+
*/
|
|
183
|
+
declare function toFieldElement(value: BigNumberish): bigint;
|
|
184
|
+
/**
|
|
185
|
+
* Coerces a pair to a Baby Jubjub public key of full order, rejecting a point
|
|
186
|
+
* that is not on the curve or that lies in the small-order subgroup.
|
|
187
|
+
*
|
|
188
|
+
* The set would happily commit to any two field elements, so without the curve
|
|
189
|
+
* check a caller could publish a commitment to a point that does not exist.
|
|
190
|
+
*
|
|
191
|
+
* The order check matters more. Under a low-order public key `A` the term `h·A`
|
|
192
|
+
* in EdDSA verification takes at most eight values whatever the message, so
|
|
193
|
+
* anyone can solve `S·B = R + h·A` for a signature over a claim tree they chose;
|
|
194
|
+
* at the identity it is immediate. Publishing such a key would hand every
|
|
195
|
+
* stranger the member's issuing power. Mirrors the same rejection in
|
|
196
|
+
* `ApprovedIssuerSet.sol`.
|
|
197
|
+
*
|
|
198
|
+
* @param publicKey The signing key as `[x, y]`.
|
|
199
|
+
* @returns The key with both coordinates as bigints.
|
|
200
|
+
*/
|
|
201
|
+
declare function toPublicKey(publicKey: IssuerPublicKey): [bigint, bigint];
|
|
202
|
+
|
|
203
|
+
export { ApprovedIssuerSet, COFACTOR, ISSUER_LEAF_DOMAIN, ISSUER_SET_MAX_DEPTH, SNARK_SCALAR_FIELD, internalNode, issuerKeyLeaf, issuerLeaf, toFieldElement, toPublicKey };
|
|
204
|
+
export type { BigNumberish, ContractLeafReader, IssuerMembershipProof, IssuerMembershipWitness, IssuerPublicKey, IssuerSet };
|