zklicensing 0.1.0-beta.2 → 0.1.0-beta.21
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/README.md +417 -201
- package/build/agent.d.ts +371 -0
- package/build/agent.d.ts.map +1 -0
- package/build/agent.js +553 -0
- package/build/agent.js.map +1 -0
- package/build/agentActions.d.ts +841 -0
- package/build/agentActions.d.ts.map +1 -0
- package/build/agentActions.js +581 -0
- package/build/agentActions.js.map +1 -0
- package/build/agentRecord.d.ts +83 -0
- package/build/agentRecord.d.ts.map +1 -0
- package/build/agentRecord.js +69 -0
- package/build/agentRecord.js.map +1 -0
- package/build/agentVendor.d.ts +220 -0
- package/build/agentVendor.d.ts.map +1 -0
- package/build/agentVendor.js +306 -0
- package/build/agentVendor.js.map +1 -0
- package/build/appsLookup.d.ts +14 -0
- package/build/appsLookup.d.ts.map +1 -0
- package/build/appsLookup.js +12 -0
- package/build/appsLookup.js.map +1 -0
- package/build/archiveBootstrap.d.ts +31 -5
- package/build/archiveBootstrap.d.ts.map +1 -0
- package/build/archiveBootstrap.js +140 -47
- package/build/archiveBootstrap.js.map +1 -0
- package/build/atomicJson.d.ts +5 -0
- package/build/atomicJson.d.ts.map +1 -0
- package/build/atomicJson.js +55 -0
- package/build/atomicJson.js.map +1 -0
- package/build/bindClient.d.ts +85 -0
- package/build/bindClient.d.ts.map +1 -0
- package/build/bindClient.js +187 -0
- package/build/bindClient.js.map +1 -0
- package/build/buyClient.d.ts +27 -4
- package/build/buyClient.d.ts.map +1 -0
- package/build/buyClient.js +72 -26
- package/build/buyClient.js.map +1 -0
- package/build/chainHead.d.ts +61 -0
- package/build/chainHead.d.ts.map +1 -0
- package/build/chainHead.js +88 -0
- package/build/chainHead.js.map +1 -0
- package/build/compactMerkleMap.d.ts +72 -0
- package/build/compactMerkleMap.d.ts.map +1 -0
- package/build/compactMerkleMap.js +224 -0
- package/build/compactMerkleMap.js.map +1 -0
- package/build/contractInterface.d.ts +191 -51
- package/build/contractInterface.d.ts.map +1 -0
- package/build/contractInterface.js +234 -43
- package/build/contractInterface.js.map +1 -0
- package/build/contractTypes.d.ts +9 -0
- package/build/contractTypes.d.ts.map +1 -0
- package/build/contractTypes.js +28 -0
- package/build/contractTypes.js.map +1 -0
- package/build/countryStats.d.ts +12 -3
- package/build/countryStats.d.ts.map +1 -0
- package/build/countryStats.js +87 -23
- package/build/countryStats.js.map +1 -0
- package/build/deployClient.d.ts +138 -24
- package/build/deployClient.d.ts.map +1 -0
- package/build/deployClient.js +91 -31
- package/build/deployClient.js.map +1 -0
- package/build/deviceIdentity.d.ts +68 -0
- package/build/deviceIdentity.d.ts.map +1 -0
- package/build/deviceIdentity.js +209 -0
- package/build/deviceIdentity.js.map +1 -0
- package/build/freezeClient.d.ts +26 -6
- package/build/freezeClient.d.ts.map +1 -0
- package/build/freezeClient.js +30 -6
- package/build/freezeClient.js.map +1 -0
- package/build/generationClient.d.ts +6 -3
- package/build/generationClient.d.ts.map +1 -0
- package/build/generationClient.js +25 -38
- package/build/generationClient.js.map +1 -0
- package/build/holderSecret.d.ts +70 -0
- package/build/holderSecret.d.ts.map +1 -0
- package/build/holderSecret.js +76 -0
- package/build/holderSecret.js.map +1 -0
- package/build/index.d.ts +38 -38
- package/build/index.d.ts.map +1 -0
- package/build/index.js +97 -121
- package/build/index.js.map +1 -0
- package/build/keysClient.d.ts +142 -0
- package/build/keysClient.d.ts.map +1 -0
- package/build/keysClient.js +109 -0
- package/build/keysClient.js.map +1 -0
- package/build/licenseLeaf.d.ts +76 -0
- package/build/licenseLeaf.d.ts.map +1 -0
- package/build/licenseLeaf.js +189 -0
- package/build/licenseLeaf.js.map +1 -0
- package/build/licenseRecord.d.ts +68 -5
- package/build/licenseRecord.d.ts.map +1 -0
- package/build/licenseRecord.js +26 -1
- package/build/licenseRecord.js.map +1 -0
- package/build/licenseStore.d.ts +81 -4
- package/build/licenseStore.d.ts.map +1 -0
- package/build/licenseStore.js +400 -30
- package/build/licenseStore.js.map +1 -0
- package/build/licenseTrustless.d.ts +43 -0
- package/build/licenseTrustless.d.ts.map +1 -0
- package/build/licenseTrustless.js +180 -0
- package/build/licenseTrustless.js.map +1 -0
- package/build/manifestVerify.d.ts +29 -1
- package/build/manifestVerify.d.ts.map +1 -0
- package/build/manifestVerify.js +46 -4
- package/build/manifestVerify.js.map +1 -0
- package/build/minaPriceHistory.d.ts +9 -0
- package/build/minaPriceHistory.d.ts.map +1 -0
- package/build/minaPriceHistory.js +152 -0
- package/build/minaPriceHistory.js.map +1 -0
- package/build/onChainRoot.d.ts +60 -0
- package/build/onChainRoot.d.ts.map +1 -0
- package/build/onChainRoot.js +74 -0
- package/build/onChainRoot.js.map +1 -0
- package/build/ownershipLicenseHash.d.ts +25 -0
- package/build/ownershipLicenseHash.d.ts.map +1 -0
- package/build/ownershipLicenseHash.js +29 -0
- package/build/ownershipLicenseHash.js.map +1 -0
- package/build/ownershipSignature.d.ts +153 -0
- package/build/ownershipSignature.d.ts.map +1 -0
- package/build/ownershipSignature.js +297 -0
- package/build/ownershipSignature.js.map +1 -0
- package/build/permitCheckFlow.d.ts +155 -0
- package/build/permitCheckFlow.d.ts.map +1 -0
- package/build/permitCheckFlow.js +196 -0
- package/build/permitCheckFlow.js.map +1 -0
- package/build/permitDescribe.d.ts +124 -0
- package/build/permitDescribe.d.ts.map +1 -0
- package/build/permitDescribe.js +324 -0
- package/build/permitDescribe.js.map +1 -0
- package/build/permitInterface.d.ts +85 -0
- package/build/permitInterface.d.ts.map +1 -0
- package/build/permitInterface.js +121 -0
- package/build/permitInterface.js.map +1 -0
- package/build/permitMessages.d.ts +313 -0
- package/build/permitMessages.d.ts.map +1 -0
- package/build/permitMessages.js +462 -0
- package/build/permitMessages.js.map +1 -0
- package/build/permitProof.d.ts +108 -0
- package/build/permitProof.d.ts.map +1 -0
- package/build/permitProof.js +202 -0
- package/build/permitProof.js.map +1 -0
- package/build/permitRegistryRebuild.d.ts +166 -0
- package/build/permitRegistryRebuild.d.ts.map +1 -0
- package/build/permitRegistryRebuild.js +347 -0
- package/build/permitRegistryRebuild.js.map +1 -0
- package/build/permitTree.d.ts +88 -0
- package/build/permitTree.d.ts.map +1 -0
- package/build/permitTree.js +230 -0
- package/build/permitTree.js.map +1 -0
- package/build/permitsClient.d.ts +680 -0
- package/build/permitsClient.d.ts.map +1 -0
- package/build/permitsClient.js +597 -0
- package/build/permitsClient.js.map +1 -0
- package/build/proveErrors.d.ts +50 -0
- package/build/proveErrors.d.ts.map +1 -0
- package/build/proveErrors.js +76 -0
- package/build/proveErrors.js.map +1 -0
- package/build/receipt.d.ts +95 -0
- package/build/receipt.d.ts.map +1 -0
- package/build/receipt.js +84 -0
- package/build/receipt.js.map +1 -0
- package/build/refundClient.d.ts +7 -12
- package/build/refundClient.d.ts.map +1 -0
- package/build/refundClient.js +52 -21
- package/build/refundClient.js.map +1 -0
- package/build/registryTrustVerify.d.ts +78 -0
- package/build/registryTrustVerify.d.ts.map +1 -0
- package/build/registryTrustVerify.js +129 -0
- package/build/registryTrustVerify.js.map +1 -0
- package/build/releaseClient.d.ts +26 -0
- package/build/releaseClient.d.ts.map +1 -0
- package/build/releaseClient.js +65 -0
- package/build/releaseClient.js.map +1 -0
- package/build/renewClient.d.ts +28 -2
- package/build/renewClient.d.ts.map +1 -0
- package/build/renewClient.js +31 -27
- package/build/renewClient.js.map +1 -0
- package/build/seatName.d.ts +8 -0
- package/build/seatName.d.ts.map +1 -0
- package/build/seatName.js +136 -0
- package/build/seatName.js.map +1 -0
- package/build/slotConstants.d.ts +15 -0
- package/build/slotConstants.d.ts.map +1 -0
- package/build/slotConstants.js +17 -0
- package/build/slotConstants.js.map +1 -0
- package/build/statsClient.d.ts +61 -7
- package/build/statsClient.d.ts.map +1 -0
- package/build/statsClient.js +44 -5
- package/build/statsClient.js.map +1 -0
- package/build/successorChain.d.ts +35 -0
- package/build/successorChain.d.ts.map +1 -0
- package/build/successorChain.js +69 -0
- package/build/successorChain.js.map +1 -0
- package/build/taxId.d.ts +44 -0
- package/build/taxId.d.ts.map +1 -0
- package/build/taxId.js +212 -0
- package/build/taxId.js.map +1 -0
- package/build/trustAnchorVerify.d.ts +49 -0
- package/build/trustAnchorVerify.d.ts.map +1 -0
- package/build/trustAnchorVerify.js +432 -0
- package/build/trustAnchorVerify.js.map +1 -0
- package/build/trustAnchorVerifyCli.d.ts +45 -0
- package/build/trustAnchorVerifyCli.d.ts.map +1 -0
- package/build/trustAnchorVerifyCli.js +235 -0
- package/build/trustAnchorVerifyCli.js.map +1 -0
- package/build/txBroadcastClient.d.ts +26 -0
- package/build/txBroadcastClient.d.ts.map +1 -0
- package/build/txBroadcastClient.js +20 -0
- package/build/txBroadcastClient.js.map +1 -0
- package/build/unbindClient.d.ts +77 -0
- package/build/unbindClient.d.ts.map +1 -0
- package/build/unbindClient.js +169 -0
- package/build/unbindClient.js.map +1 -0
- package/build/units.d.ts +28 -0
- package/build/units.d.ts.map +1 -0
- package/build/units.js +65 -0
- package/build/units.js.map +1 -0
- package/build/vatRates.d.ts +54 -0
- package/build/vatRates.d.ts.map +1 -0
- package/build/vatRates.js +82 -0
- package/build/vatRates.js.map +1 -0
- package/build/vatRatesData.d.ts +5 -0
- package/build/vatRatesData.d.ts.map +1 -0
- package/build/vatRatesData.js +42 -0
- package/build/vatRatesData.js.map +1 -0
- package/build/vendorAccountClient.d.ts +66 -0
- package/build/vendorAccountClient.d.ts.map +1 -0
- package/build/vendorAccountClient.js +47 -0
- package/build/vendorAccountClient.js.map +1 -0
- package/build/verifyCore.d.ts +4 -1
- package/build/verifyCore.d.ts.map +1 -0
- package/build/verifyCore.js +21 -10
- package/build/verifyCore.js.map +1 -0
- package/build/verifyOffline.d.ts +85 -0
- package/build/verifyOffline.d.ts.map +1 -0
- package/build/verifyOffline.js +139 -0
- package/build/verifyOffline.js.map +1 -0
- package/build/verifyService.d.ts.map +1 -0
- package/build/verifyService.js +571 -501
- package/build/verifyService.js.map +1 -0
- package/build/verifyWithAncestors.d.ts +19 -0
- package/build/verifyWithAncestors.d.ts.map +1 -0
- package/build/verifyWithAncestors.js +127 -0
- package/build/verifyWithAncestors.js.map +1 -0
- package/build/zipToState.d.ts +2 -0
- package/build/zipToState.d.ts.map +1 -0
- package/build/zipToState.js +94 -0
- package/build/zipToState.js.map +1 -0
- package/package.json +141 -16
- package/src/agent.ts +682 -0
- package/src/agentActions.ts +637 -0
- package/src/agentRecord.ts +121 -0
- package/src/agentVendor.ts +356 -0
- package/src/appsLookup.ts +48 -0
- package/src/archiveBootstrap.ts +386 -0
- package/src/atomicJson.ts +53 -0
- package/src/bindClient.ts +297 -0
- package/src/buyClient.ts +344 -0
- package/src/chainHead.ts +106 -0
- package/src/compactMerkleMap.ts +222 -0
- package/src/contractInterface.ts +465 -0
- package/src/contractTypes.ts +31 -0
- package/src/countryStats.ts +200 -0
- package/src/deployClient.ts +517 -0
- package/src/deviceIdentity.ts +249 -0
- package/src/freezeClient.ts +162 -0
- package/src/generationClient.ts +145 -0
- package/src/holderSecret.ts +91 -0
- package/src/index.ts +362 -0
- package/src/keysClient.ts +135 -0
- package/src/licenseLeaf.ts +221 -0
- package/src/licenseRecord.ts +190 -0
- package/src/licenseStore.ts +767 -0
- package/src/licenseTrustless.ts +207 -0
- package/src/manifestVerify.ts +164 -0
- package/src/minaPriceHistory.ts +166 -0
- package/src/onChainRoot.ts +90 -0
- package/src/ownershipLicenseHash.ts +31 -0
- package/src/ownershipSignature.ts +421 -0
- package/src/permitCheckFlow.ts +227 -0
- package/src/permitDescribe.ts +362 -0
- package/src/permitInterface.ts +146 -0
- package/src/permitMessages.ts +572 -0
- package/src/permitProof.ts +230 -0
- package/src/permitRegistryRebuild.ts +423 -0
- package/src/permitTree.ts +232 -0
- package/src/permitsClient.ts +746 -0
- package/src/proveErrors.ts +98 -0
- package/src/receipt.ts +156 -0
- package/src/refundClient.ts +177 -0
- package/src/registryTrustVerify.ts +192 -0
- package/src/releaseClient.ts +101 -0
- package/src/renewClient.ts +234 -0
- package/src/seatName.ts +165 -0
- package/src/slotConstants.ts +18 -0
- package/src/statsClient.ts +249 -0
- package/src/successorChain.ts +87 -0
- package/src/taxId.ts +227 -0
- package/src/trustAnchorVerify.ts +550 -0
- package/src/trustAnchorVerifyCli.ts +234 -0
- package/src/txBroadcastClient.ts +48 -0
- package/src/unbindClient.ts +233 -0
- package/src/units.ts +74 -0
- package/src/vatRates.ts +100 -0
- package/src/vatRatesData.ts +43 -0
- package/src/vendorAccountClient.ts +120 -0
- package/src/verifyCore.ts +100 -0
- package/src/verifyOffline.ts +212 -0
- package/src/verifyService.ts +982 -0
- package/src/verifyWithAncestors.ts +163 -0
- package/src/zipToState.ts +94 -0
- package/build/licenseProof.d.ts +0 -26
- package/build/licenseProof.js +0 -41
- package/build/migrateClient.d.ts +0 -43
- package/build/migrateClient.js +0 -47
- package/build/ownership.d.ts +0 -34
- package/build/ownership.js +0 -147
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* permitProof.ts — `zklicensing/permits/proof`
|
|
6
|
+
*
|
|
7
|
+
* Zero-knowledge proof that the prover holds an active permit in a registry
|
|
8
|
+
* (PERMITS_PLAN.md §7.2), without revealing which permit or which key:
|
|
9
|
+
*
|
|
10
|
+
* public: registry, root, slot, requiredScope, context
|
|
11
|
+
* output: nullifier = Poseidon(holder secret, context)
|
|
12
|
+
* private: the permit leaf, its Merkle witness, the holder secret, and the
|
|
13
|
+
* holder's signature over (registry, root, context)
|
|
14
|
+
*
|
|
15
|
+
* The verifier checks the proof, that `root` is a recent root of the
|
|
16
|
+
* registry (from a Mina node, or the keeper's /roots), that `slot` is close
|
|
17
|
+
* to now, and — if it wants one use per holder per context — that the
|
|
18
|
+
* nullifier is new. Proving runs on the holder's device: it needs the
|
|
19
|
+
* holder's signature and secret, which never leave it.
|
|
20
|
+
*
|
|
21
|
+
* The holder secret: permit subjects are public (each permit is published
|
|
22
|
+
* with its subject), so a nullifier computed from the subject could be
|
|
23
|
+
* recomputed by anyone for every permit and matched. The nullifier is
|
|
24
|
+
* computed from a secret only the holder's wallet can produce instead: the
|
|
25
|
+
* hash of its text signature over (registry, network, a counter), which is
|
|
26
|
+
* deterministic, so the same wallet re-derives it anywhere from the chain
|
|
27
|
+
* alone (holderSecret.ts). The permit's
|
|
28
|
+
* salt is the hash of that secret, so the keeper and the authority, who see
|
|
29
|
+
* the salt, still cannot compute nullifiers; and one secret per permit keeps
|
|
30
|
+
* one nullifier per permit and context.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { Field, MerkleMapWitness, Poseidon, PublicKey, Signature, Struct, UInt32, ZkProgram } from 'o1js';
|
|
34
|
+
import {
|
|
35
|
+
PermitLeaf, permitLeafHash, permitSubject, PERMIT_STATUS_ACTIVE,
|
|
36
|
+
} from './permitMessages.js';
|
|
37
|
+
import { saltFromHolderSecret } from './holderSecret.js';
|
|
38
|
+
|
|
39
|
+
/** Tag for the holder's proof signature (distinct from every registry message). */
|
|
40
|
+
export const PERMIT_PROOF_TAG: Field = Field([...'zkPermits/v1/proof'].reduce((v, ch) => (v << 8n) | BigInt(ch.charCodeAt(0)), 0n));
|
|
41
|
+
|
|
42
|
+
export const PermitProofInput: any = Struct({
|
|
43
|
+
registry: PublicKey,
|
|
44
|
+
root: Field,
|
|
45
|
+
slot: UInt32,
|
|
46
|
+
requiredScope: Field,
|
|
47
|
+
context: Field,
|
|
48
|
+
});
|
|
49
|
+
export type PermitProofInput = any;
|
|
50
|
+
|
|
51
|
+
/** The message the holder signs to prove control of the permit's key. */
|
|
52
|
+
export function holderProofMessage(registry: PublicKey, root: Field, context: Field): Field[] {
|
|
53
|
+
return [PERMIT_PROOF_TAG, ...registry.toFields(), root, context];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Nullifier: stable per (permit, context), unlinkable across contexts; needs the holder secret. */
|
|
57
|
+
export function permitNullifier(holderSecret: Field, context: Field): Field {
|
|
58
|
+
return Poseidon.hash([PERMIT_PROOF_TAG, holderSecret, context]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Holder secret ─── (holderSecret.ts; re-exported here)
|
|
62
|
+
|
|
63
|
+
export {
|
|
64
|
+
HOLDER_SECRET_TAG, HOLDER_SALT_TAG, holderSecretText, holderSecretFromSignature, saltFromHolderSecret, deriveHolderSecret,
|
|
65
|
+
findHolderSecret, holderSecretMatches, MAX_HOLDER_SECRETS,
|
|
66
|
+
} from './holderSecret.js';
|
|
67
|
+
|
|
68
|
+
// ── Verifier-bound challenges ───────────────────────────────────────────────
|
|
69
|
+
//
|
|
70
|
+
// The proof's `context` is a public input the verifier chooses, and the
|
|
71
|
+
// nullifier is Poseidon(tag, subject, context). If two verifiers handed out
|
|
72
|
+
// the same raw context, the same holder would give both the same nullifier,
|
|
73
|
+
// and comparing lists would link the two uses. So the context is derived
|
|
74
|
+
// from the verifier's identity and its nonce: verifiers with different
|
|
75
|
+
// identities always get different contexts, whatever nonces they pick. The
|
|
76
|
+
// holder derives it from the identity it actually talks to (a site's
|
|
77
|
+
// origin, a service's address), never from a context the verifier supplies.
|
|
78
|
+
|
|
79
|
+
/** Tag for verifier-bound contexts (distinct from PERMIT_PROOF_TAG). */
|
|
80
|
+
export const VERIFIER_CONTEXT_TAG: Field = Field([...'zkPermits/v1/verifier'].reduce((v, ch) => (v << 8n) | BigInt(ch.charCodeAt(0)), 0n));
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The canonical form of a verifier identity: a URL becomes its origin
|
|
84
|
+
* (scheme, lower-case host, non-default port; path and query dropped), so
|
|
85
|
+
* `https://Shop.Example/verify?x=1` and `https://shop.example` are the same
|
|
86
|
+
* verifier. Anything else is a service name, trimmed and lower-cased with
|
|
87
|
+
* runs of whitespace collapsed.
|
|
88
|
+
*/
|
|
89
|
+
export function normalizeVerifier(verifier: string): string {
|
|
90
|
+
const raw = String(verifier ?? '').trim();
|
|
91
|
+
let id: string;
|
|
92
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) {
|
|
93
|
+
let origin: string;
|
|
94
|
+
try { origin = new URL(raw).origin; } catch { throw new Error(`verifier is not a valid URL: ${raw}`); }
|
|
95
|
+
if (!origin || origin === 'null') throw new Error(`verifier URL has no origin: ${raw}`);
|
|
96
|
+
id = origin;
|
|
97
|
+
} else {
|
|
98
|
+
id = raw.toLowerCase().replace(/\s+/g, ' ');
|
|
99
|
+
}
|
|
100
|
+
if (!id) throw new Error('verifier identity is empty');
|
|
101
|
+
if (id.length > 256) throw new Error('verifier identity is longer than 256 characters');
|
|
102
|
+
if (/[\u0000-\u001f\u007f]/.test(id)) throw new Error('verifier identity contains control characters');
|
|
103
|
+
return id;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A fresh 128-bit nonce for a verifier to hand to a holder. */
|
|
107
|
+
export function randomNonce(): bigint {
|
|
108
|
+
const b = new Uint8Array(16);
|
|
109
|
+
globalThis.crypto.getRandomValues(b);
|
|
110
|
+
return b.reduce((v, x) => (v << 8n) | BigInt(x), 0n);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseNonce(nonce: bigint | string): bigint {
|
|
114
|
+
const n = typeof nonce === 'bigint' ? nonce : /^\d{1,80}$/.test(String(nonce).trim()) ? BigInt(String(nonce).trim()) : -1n;
|
|
115
|
+
if (n < 0n || n >= 1n << 253n) throw new Error('nonce must be a decimal integer below 2^253');
|
|
116
|
+
return n;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The proof context for `verifier` (see normalizeVerifier) and its `nonce`:
|
|
121
|
+
* Poseidon(VERIFIER_CONTEXT_TAG, byteLength, identity in 31-byte chunks, nonce).
|
|
122
|
+
* Holder and verifier compute it the same way.
|
|
123
|
+
*/
|
|
124
|
+
export function verifierContext(verifier: string, nonce: bigint | string): bigint {
|
|
125
|
+
const bytes = new TextEncoder().encode(normalizeVerifier(verifier));
|
|
126
|
+
const chunks: Field[] = [];
|
|
127
|
+
for (let i = 0; i < bytes.length; i += 31) {
|
|
128
|
+
chunks.push(Field(bytes.slice(i, i + 31).reduce((v, x) => (v << 8n) | BigInt(x), 0n)));
|
|
129
|
+
}
|
|
130
|
+
return Poseidon.hash([VERIFIER_CONTEXT_TAG, Field(bytes.length), ...chunks, Field(parseNonce(nonce))]).toBigInt();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** In-circuit: every bit set in `required` is set in `scope` (254-bit masks). */
|
|
134
|
+
function assertScopeCovers(scope: Field, required: Field): void {
|
|
135
|
+
const s = scope.toBits(254);
|
|
136
|
+
const r = required.toBits(254);
|
|
137
|
+
for (let i = 0; i < 254; i++) r[i].not().or(s[i]).assertTrue();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const PermitProof: any = ZkProgram({
|
|
141
|
+
name: 'zkPermits-holder-proof',
|
|
142
|
+
publicInput: PermitProofInput,
|
|
143
|
+
publicOutput: Field,
|
|
144
|
+
methods: {
|
|
145
|
+
prove: {
|
|
146
|
+
privateInputs: [PermitLeaf, MerkleMapWitness, Field, Signature],
|
|
147
|
+
async method(input: PermitProofInput, leaf: PermitLeaf, witness: MerkleMapWitness, holderSecret: Field, holderSig: Signature) {
|
|
148
|
+
const [root, key] = witness.computeRootAndKey(permitLeafHash(leaf));
|
|
149
|
+
root.assertEquals(input.root);
|
|
150
|
+
key.assertEquals(leaf.subject);
|
|
151
|
+
permitSubject(leaf.holderKey, saltFromHolderSecret(holderSecret)).assertEquals(leaf.subject);
|
|
152
|
+
holderSig.verify(leaf.holderKey, holderProofMessage(input.registry, input.root, input.context)).assertTrue();
|
|
153
|
+
leaf.status.assertEquals(PERMIT_STATUS_ACTIVE);
|
|
154
|
+
leaf.validFrom.assertLessThanOrEqual(input.slot);
|
|
155
|
+
input.slot.assertLessThanOrEqual(leaf.expirySlot);
|
|
156
|
+
assertScopeCovers(leaf.scope, input.requiredScope);
|
|
157
|
+
return { publicOutput: permitNullifier(holderSecret, input.context) };
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
export type HolderProofRequest = {
|
|
164
|
+
registry: string;
|
|
165
|
+
/** Root the witness is for (the keeper's GET …/permits/:subject returns both). */
|
|
166
|
+
root: string;
|
|
167
|
+
witness: { isLefts: boolean[]; siblings: string[] };
|
|
168
|
+
leaf: PermitLeaf;
|
|
169
|
+
/** The holder secret (deriveHolderSecret). Never leaves the holder's device. */
|
|
170
|
+
holderSecret: string;
|
|
171
|
+
slot: number;
|
|
172
|
+
requiredScope: bigint;
|
|
173
|
+
context: bigint;
|
|
174
|
+
/** Signature over holderProofMessage(registry, root, context) by the holder key (wallet signFields). */
|
|
175
|
+
holderSignature: string;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** Holder side: compiles (once) and proves. Returns the proof as JSON. */
|
|
179
|
+
export async function proveHolder(r: HolderProofRequest): Promise<{ proof: unknown; nullifier: string }> {
|
|
180
|
+
const { Bool } = await import('o1js');
|
|
181
|
+
await PermitProof.compile();
|
|
182
|
+
const input = new PermitProofInput({
|
|
183
|
+
registry: PublicKey.fromBase58(r.registry),
|
|
184
|
+
root: Field(r.root),
|
|
185
|
+
slot: UInt32.from(r.slot),
|
|
186
|
+
requiredScope: Field(r.requiredScope),
|
|
187
|
+
context: Field(r.context),
|
|
188
|
+
});
|
|
189
|
+
const witness = new MerkleMapWitness(r.witness.isLefts.map(b => Bool(b)), r.witness.siblings.map(s => Field(s)));
|
|
190
|
+
const { proof } = await PermitProof.prove(input, r.leaf, witness, Field(r.holderSecret), Signature.fromBase58(r.holderSignature));
|
|
191
|
+
return { proof: proof.toJSON(), nullifier: proof.publicOutput.toString() };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export type HolderProofCheck = {
|
|
195
|
+
registry: string;
|
|
196
|
+
/** Roots accepted as current (e.g. from a Mina node, or the keeper's recent /roots). */
|
|
197
|
+
acceptedRoots: string[];
|
|
198
|
+
currentSlot: number;
|
|
199
|
+
/** How far the proof's slot may be from `currentSlot`. Default 10 slots. */
|
|
200
|
+
maxSlotSkew?: number;
|
|
201
|
+
requiredScope?: bigint;
|
|
202
|
+
} & (
|
|
203
|
+
/** Your identity as the holder saw it and the nonce you issued: the expected context is verifierContext(verifier, nonce). */
|
|
204
|
+
| { verifier: string; nonce: bigint | string; context?: never }
|
|
205
|
+
/** A raw expected context, for schemes that derive it otherwise. */
|
|
206
|
+
| { context: bigint; verifier?: never; nonce?: never }
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
/** Verifier side: checks the proof and its public inputs. Returns the nullifier. */
|
|
210
|
+
export async function verifyHolderProof(proofJson: unknown, check: HolderProofCheck): Promise<{ valid: boolean; nullifier: string | null; reason: string | null }> {
|
|
211
|
+
const { verificationKey } = await PermitProof.compile();
|
|
212
|
+
const ProofClass = ZkProgram.Proof(PermitProof);
|
|
213
|
+
let proof: any;
|
|
214
|
+
try { proof = await ProofClass.fromJSON(proofJson as any); } catch (e: any) {
|
|
215
|
+
return { valid: false, nullifier: null, reason: `malformed proof: ${e?.message ?? e}` };
|
|
216
|
+
}
|
|
217
|
+
const pub = proof.publicInput;
|
|
218
|
+
const fail = (reason: string) => ({ valid: false, nullifier: null, reason });
|
|
219
|
+
if (pub.registry.toBase58() !== check.registry) return fail('proof is for another registry');
|
|
220
|
+
if (!check.acceptedRoots.includes(pub.root.toString())) return fail('root is not a current root of the registry');
|
|
221
|
+
if (Math.abs(Number(pub.slot.toString()) - check.currentSlot) > (check.maxSlotSkew ?? 10)) return fail('slot is too far from now');
|
|
222
|
+
if (pub.requiredScope.toBigInt() !== (check.requiredScope ?? 0n)) return fail('proof is for other scope bits');
|
|
223
|
+
let expected: bigint;
|
|
224
|
+
try { expected = check.verifier !== undefined ? verifierContext(check.verifier, check.nonce) : check.context; }
|
|
225
|
+
catch (e: any) { return fail(e?.message ?? String(e)); }
|
|
226
|
+
if (pub.context.toBigInt() !== expected) return fail('proof is for another context');
|
|
227
|
+
const { verify } = await import('o1js');
|
|
228
|
+
if (!(await verify(proof, verificationKey))) return fail('proof does not verify');
|
|
229
|
+
return { valid: true, nullifier: proof.publicOutput.toString(), reason: null };
|
|
230
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// Copyright (c) 2025-2026 zkLicensing project developers
|
|
2
|
+
// SPDX-License-Identifier: BUSL-1.1
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* permitRegistryRebuild.ts — `zklicensing/permits/rebuild`
|
|
6
|
+
*
|
|
7
|
+
* Rebuilds a PermitRegistry's permits tree from its events alone. Every
|
|
8
|
+
* event that changes a permit carries the full new leaf, so the tree is
|
|
9
|
+
* recoverable from the chain without the Keeper (PERMITS_PLAN.md §8.2).
|
|
10
|
+
* The Keeper's indexer uses this same code. Open applications (fees in
|
|
11
|
+
* escrow) are rebuilt the same way: filed minus settled.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Field, PublicKey, UInt32, UInt64 } from 'o1js';
|
|
15
|
+
import { CompactMerkleMap, encodeFields, decodeFields } from './compactMerkleMap.js';
|
|
16
|
+
import { Application, PermitLeaf, applicationHash, permitLeafHash, unpackFees, unpackPolicy } from './permitMessages.js';
|
|
17
|
+
import { PermitRegistryEvents } from './permitInterface.js';
|
|
18
|
+
import { forkBumpedExpiry } from './contractInterface.js';
|
|
19
|
+
|
|
20
|
+
/** A decoded event, as returned by `fetchEvents` (fields we use). */
|
|
21
|
+
export type RegistryEvent = {
|
|
22
|
+
type: string;
|
|
23
|
+
event: { data: any };
|
|
24
|
+
blockHeight?: UInt32 | { toString(): string };
|
|
25
|
+
globalSlot?: UInt32 | { toString(): string };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type PermitRecord = {
|
|
29
|
+
subject: string;
|
|
30
|
+
holderKey: string;
|
|
31
|
+
scope: string;
|
|
32
|
+
validFrom: number;
|
|
33
|
+
expirySlot: number;
|
|
34
|
+
status: number;
|
|
35
|
+
seq: number;
|
|
36
|
+
docHash: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** An application with its fee in escrow, as JSON (amounts in nanomina, slots as numbers). */
|
|
40
|
+
export type ApplicationRecord = {
|
|
41
|
+
subject: string;
|
|
42
|
+
holderKey: string;
|
|
43
|
+
payer: string;
|
|
44
|
+
baseSeq: number;
|
|
45
|
+
scope: string;
|
|
46
|
+
validFrom: number;
|
|
47
|
+
expirySlot: number;
|
|
48
|
+
docHash: string;
|
|
49
|
+
fee: string;
|
|
50
|
+
processingFee: string;
|
|
51
|
+
deadline: number;
|
|
52
|
+
/** The registry's filing number. */
|
|
53
|
+
filing: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** How an application ended (APPLICATION_OUTCOME), amounts in nanomina. */
|
|
57
|
+
export type Settlement = { subject: string; outcome: number; kept: string; refunded: string };
|
|
58
|
+
|
|
59
|
+
export type RegistryView = {
|
|
60
|
+
/** The permits tree (a MerkleMap drop-in that can be saved and loaded without re-hashing). */
|
|
61
|
+
tree: CompactMerkleMap;
|
|
62
|
+
root: string;
|
|
63
|
+
permits: Map<string, PermitRecord>;
|
|
64
|
+
keysHash: string | null;
|
|
65
|
+
epoch: number;
|
|
66
|
+
feesPacked: string | null;
|
|
67
|
+
/** The escrow tree (subject → applicationHash of the open application) and its root. */
|
|
68
|
+
escrow: CompactMerkleMap;
|
|
69
|
+
escrowRoot: string;
|
|
70
|
+
/** Open applications by subject. */
|
|
71
|
+
applications: Map<string, ApplicationRecord>;
|
|
72
|
+
/** Settled applications by application hash: how each ended. */
|
|
73
|
+
settlements: Map<string, Settlement>;
|
|
74
|
+
eventsApplied: number;
|
|
75
|
+
/** Set once the registry was retired (redeployed): its frozen permits root. */
|
|
76
|
+
retiredRoot?: string;
|
|
77
|
+
/** Whether the platform key retired it (retireByPlatform) rather than the authority. */
|
|
78
|
+
retiredByPlatform?: boolean;
|
|
79
|
+
/** Whether it was retired with application fees in escrow (they stay on it). */
|
|
80
|
+
retiredForced?: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* A redeployed registry starts with its predecessor's permits
|
|
83
|
+
* (seededRegistryView): the tree it started with, and each carried-over
|
|
84
|
+
* permit's seq then, so its next change is seq + 1.
|
|
85
|
+
*/
|
|
86
|
+
start?: { root: string; seqs: Map<string, number> };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export function leafToRecord(leaf: PermitLeaf): PermitRecord {
|
|
90
|
+
return {
|
|
91
|
+
subject: leaf.subject.toString(),
|
|
92
|
+
// The never-granted leaf's key is PublicKey.empty() (not a curve point): ''.
|
|
93
|
+
holderKey: leaf.holderKey.equals(PublicKey.empty()).toBoolean() ? '' : leaf.holderKey.toBase58(),
|
|
94
|
+
scope: leaf.scope.toString(),
|
|
95
|
+
validFrom: Number(leaf.validFrom.toString()),
|
|
96
|
+
expirySlot: Number(leaf.expirySlot.toString()),
|
|
97
|
+
status: Number(leaf.status.toString()),
|
|
98
|
+
seq: Number(leaf.seq.toString()),
|
|
99
|
+
docHash: leaf.docHash.toString(),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function applicationToRecord(app: Application): ApplicationRecord {
|
|
104
|
+
return {
|
|
105
|
+
subject: app.subject.toString(),
|
|
106
|
+
holderKey: app.holderKey.toBase58(),
|
|
107
|
+
payer: app.payer.toBase58(),
|
|
108
|
+
baseSeq: Number(app.baseSeq.toString()),
|
|
109
|
+
scope: app.scope.toString(),
|
|
110
|
+
validFrom: Number(app.validFrom.toString()),
|
|
111
|
+
expirySlot: Number(app.expirySlot.toString()),
|
|
112
|
+
docHash: app.docHash.toString(),
|
|
113
|
+
fee: app.fee.toString(),
|
|
114
|
+
processingFee: app.processingFee.toString(),
|
|
115
|
+
deadline: Number(app.deadline.toString()),
|
|
116
|
+
filing: Number(app.filing.toString()),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function recordToApplication(r: ApplicationRecord): Application {
|
|
121
|
+
return new Application({
|
|
122
|
+
subject: Field(r.subject),
|
|
123
|
+
holderKey: PublicKey.fromBase58(r.holderKey),
|
|
124
|
+
payer: PublicKey.fromBase58(r.payer),
|
|
125
|
+
baseSeq: UInt32.from(r.baseSeq),
|
|
126
|
+
scope: Field(r.scope),
|
|
127
|
+
validFrom: UInt32.from(r.validFrom),
|
|
128
|
+
expirySlot: UInt32.from(r.expirySlot),
|
|
129
|
+
docHash: Field(r.docHash),
|
|
130
|
+
fee: UInt64.from(r.fee),
|
|
131
|
+
processingFee: UInt64.from(r.processingFee),
|
|
132
|
+
deadline: UInt32.from(r.deadline),
|
|
133
|
+
filing: UInt32.from(r.filing),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function recordToLeaf(r: PermitRecord): PermitLeaf {
|
|
138
|
+
return new PermitLeaf({
|
|
139
|
+
subject: Field(r.subject),
|
|
140
|
+
holderKey: r.holderKey === '' ? PublicKey.empty() : PublicKey.fromBase58(r.holderKey),
|
|
141
|
+
scope: Field(r.scope),
|
|
142
|
+
validFrom: UInt32.from(r.validFrom),
|
|
143
|
+
expirySlot: UInt32.from(r.expirySlot),
|
|
144
|
+
status: UInt32.from(r.status),
|
|
145
|
+
seq: UInt32.from(r.seq),
|
|
146
|
+
docHash: Field(r.docHash),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The view a redeployed registry starts with: a copy of its retired
|
|
152
|
+
* predecessor's permits and tree (the initialPermitsRoot its keys signed).
|
|
153
|
+
* Its own events then apply on top; `registryCreated` checks the root.
|
|
154
|
+
*/
|
|
155
|
+
export function seededRegistryView(predecessor: RegistryView): RegistryView {
|
|
156
|
+
const view = registryViewFromStored({ ...registryViewToStored(predecessor), applications: [] });
|
|
157
|
+
view.start = { root: view.root, seqs: new Map([...view.permits.values()].map(p => [p.subject, p.seq] as const)) };
|
|
158
|
+
return view;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The retired registry a registry continues, from its `registryCreated`
|
|
163
|
+
* event (base58), or null for a first generation. With it a registry's
|
|
164
|
+
* lineage can be walked back on chain events alone; the successor's start
|
|
165
|
+
* was checked against the predecessor's retired root when it was deployed.
|
|
166
|
+
*/
|
|
167
|
+
export function registryPredecessor(events: RegistryEvent[]): string | null {
|
|
168
|
+
const created = events.find(e => e.type === 'registryCreated');
|
|
169
|
+
const p = created?.event.data.predecessor as PublicKey | undefined;
|
|
170
|
+
if (!p || p.equals(PublicKey.empty()).toBoolean()) return null;
|
|
171
|
+
return p.toBase58();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function emptyRegistryView(): RegistryView {
|
|
175
|
+
const tree = new CompactMerkleMap();
|
|
176
|
+
return { tree, root: tree.getRoot().toString(), permits: new Map(), keysHash: null, epoch: 0, feesPacked: null, ...emptyEscrow(), eventsApplied: 0 };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function emptyEscrow() {
|
|
180
|
+
const escrow = new CompactMerkleMap();
|
|
181
|
+
return { escrow, escrowRoot: escrow.getRoot().toString(), applications: new Map<string, ApplicationRecord>(), settlements: new Map<string, Settlement>() };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Open applications from filed and settled events. A settled event names the
|
|
186
|
+
* application by hash; the same hash can be filed again after it settles, so
|
|
187
|
+
* filings and settlements are counted. Events of a transaction the chain
|
|
188
|
+
* refused can show up in some sources (the local test chain records them):
|
|
189
|
+
* a settlement with no filing is ignored, and of two open filings for one
|
|
190
|
+
* subject the later filing wins, since a subject can only be filed again
|
|
191
|
+
* once its previous application settled. The view's escrowRoot against the
|
|
192
|
+
* contract's is the check that the result is right.
|
|
193
|
+
*/
|
|
194
|
+
function openApplications(filed: Application[], settled: string[]): Map<string, ApplicationRecord> {
|
|
195
|
+
const closed = new Map<string, number>();
|
|
196
|
+
for (const h of settled) closed.set(h, (closed.get(h) ?? 0) + 1);
|
|
197
|
+
const open = new Map<string, ApplicationRecord>();
|
|
198
|
+
for (const app of filed) {
|
|
199
|
+
const h = applicationHash(app).toString();
|
|
200
|
+
const n = closed.get(h) ?? 0;
|
|
201
|
+
if (n > 0) { closed.set(h, n - 1); continue; }
|
|
202
|
+
const rec = applicationToRecord(app);
|
|
203
|
+
const other = open.get(rec.subject);
|
|
204
|
+
if (!other || rec.filing > other.filing) open.set(rec.subject, rec);
|
|
205
|
+
}
|
|
206
|
+
return open;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Replaces the view's open applications and rebuilds the (small) escrow tree. */
|
|
210
|
+
function setApplications(view: RegistryView, open: Map<string, ApplicationRecord>) {
|
|
211
|
+
const escrow = new CompactMerkleMap();
|
|
212
|
+
escrow.setMany([...open.values()].map(r => [Field(r.subject), applicationHash(recordToApplication(r))] as [Field, Field]));
|
|
213
|
+
view.escrow = escrow;
|
|
214
|
+
view.escrowRoot = escrow.getRoot().toString();
|
|
215
|
+
view.applications = open;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Applies events in any order. Archive nodes, local chains and paginated
|
|
220
|
+
* fetches don't agree on ordering, so none is assumed: each subject's leaves
|
|
221
|
+
* are ordered by `seq` (the contract advances it by exactly one per change),
|
|
222
|
+
* key rotations by `epoch` and fee changes by `actionNonce`. A gap in a
|
|
223
|
+
* subject's sequence means events are missing, and throws rather than build
|
|
224
|
+
* a wrong tree. Applying to a non-empty view continues from its state.
|
|
225
|
+
*/
|
|
226
|
+
export function applyRegistryEvents(view: RegistryView, events: RegistryEvent[]): RegistryView {
|
|
227
|
+
const updates: Array<[Field, Field]> = [];
|
|
228
|
+
const esc = { filed: [...view.applications.values()].map(recordToApplication), settled: [] as string[] };
|
|
229
|
+
for (const [subject, last] of collectRegistryEvents(view, events, esc)) {
|
|
230
|
+
updates.push([last.leaf.subject, permitLeafHash(last.leaf)]);
|
|
231
|
+
view.permits.set(subject, last.rec);
|
|
232
|
+
}
|
|
233
|
+
view.tree.setMany(updates);
|
|
234
|
+
view.root = view.tree.getRoot().toString();
|
|
235
|
+
setApplications(view, openApplications(esc.filed, esc.settled));
|
|
236
|
+
return view;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Brings `view` up to date with a registry's full event list, re-hashing only
|
|
241
|
+
* the permits whose latest leaf changed: a full rebuild costs a 256-level tree
|
|
242
|
+
* path per permit, a refresh one per changed permit. Returns the subjects
|
|
243
|
+
* that changed (added or updated).
|
|
244
|
+
*/
|
|
245
|
+
export function refreshRegistryView(view: RegistryView, events: RegistryEvent[]): string[] {
|
|
246
|
+
const fresh: RegistryView = { tree: view.tree, root: view.root, permits: new Map(), keysHash: null, epoch: 0, feesPacked: null, ...emptyEscrow(), eventsApplied: 0, ...(view.start ? { start: view.start } : {}) };
|
|
247
|
+
const esc = { filed: [] as Application[], settled: [] as string[] };
|
|
248
|
+
const latest = collectRegistryEvents(fresh, events, esc);
|
|
249
|
+
const changed: string[] = [];
|
|
250
|
+
const updates: Array<[Field, Field]> = [];
|
|
251
|
+
for (const [subject, last] of latest) {
|
|
252
|
+
const cur = view.permits.get(subject);
|
|
253
|
+
if (cur && sameRecord(cur, last.rec)) continue;
|
|
254
|
+
updates.push([last.leaf.subject, permitLeafHash(last.leaf)]);
|
|
255
|
+
view.permits.set(subject, last.rec);
|
|
256
|
+
changed.push(subject);
|
|
257
|
+
}
|
|
258
|
+
view.tree.setMany(updates);
|
|
259
|
+
view.keysHash = fresh.keysHash; view.epoch = fresh.epoch; view.feesPacked = fresh.feesPacked; view.eventsApplied = fresh.eventsApplied;
|
|
260
|
+
view.settlements = fresh.settlements;
|
|
261
|
+
view.root = view.tree.getRoot().toString();
|
|
262
|
+
setApplications(view, openApplications(esc.filed, esc.settled));
|
|
263
|
+
return changed;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Same permit leaf (field by field: cheaper than hashing both). */
|
|
267
|
+
function sameRecord(a: PermitRecord, b: PermitRecord): boolean {
|
|
268
|
+
return a.seq === b.seq && a.status === b.status && a.scope === b.scope && a.validFrom === b.validFrom
|
|
269
|
+
&& a.expirySlot === b.expirySlot && a.holderKey === b.holderKey && a.docHash === b.docHash && a.subject === b.subject;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Validates the events and returns each subject's latest leaf; updates the
|
|
274
|
+
* view's keys, epoch and fees, and collects escrow events into `esc`.
|
|
275
|
+
*/
|
|
276
|
+
function collectRegistryEvents(view: RegistryView, events: RegistryEvent[], esc: { filed: Application[]; settled: string[] }): Map<string, { type: string; leaf: PermitLeaf; rec: PermitRecord }> {
|
|
277
|
+
const bySubject = new Map<string, Array<{ type: string; leaf: PermitLeaf; rec: PermitRecord }>>();
|
|
278
|
+
let lastEpoch = view.epoch;
|
|
279
|
+
let lastFeeNonce = -1;
|
|
280
|
+
for (const e of events) {
|
|
281
|
+
const d = e.event.data;
|
|
282
|
+
switch (e.type) {
|
|
283
|
+
case 'registryCreated': {
|
|
284
|
+
if (view.keysHash === null) view.keysHash = d.keysHash.toString();
|
|
285
|
+
const startRoot = view.start?.root ?? emptyRegistryView().root;
|
|
286
|
+
if (d.permitsRoot !== undefined && d.permitsRoot.toString() !== startRoot) {
|
|
287
|
+
throw new Error('the registry started from another permits tree: seed the view with its predecessor (seededRegistryView)');
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
case 'authorityRotated': {
|
|
292
|
+
const epoch = Number(d.epoch.toString());
|
|
293
|
+
if (epoch > lastEpoch || view.keysHash === null) { lastEpoch = epoch; view.keysHash = d.keysHash.toString(); view.epoch = epoch; }
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
case 'feesUpdated': {
|
|
297
|
+
const nonce = Number(d.actionNonce.toString());
|
|
298
|
+
if (nonce > lastFeeNonce) { lastFeeNonce = nonce; view.feesPacked = d.feesPacked.toString(); }
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
// A permit moved to a new holder key: its new leaf (seq 1, a new subject);
|
|
302
|
+
// the old one closes as MOVED in a permitStatusChanged event.
|
|
303
|
+
case 'permitMoved':
|
|
304
|
+
case 'permitGranted':
|
|
305
|
+
case 'permitStatusChanged': {
|
|
306
|
+
const leaf = d.leaf as PermitLeaf;
|
|
307
|
+
const rec = leafToRecord(leaf);
|
|
308
|
+
const list = bySubject.get(rec.subject) ?? [];
|
|
309
|
+
list.push({ type: e.type, leaf, rec });
|
|
310
|
+
bySubject.set(rec.subject, list);
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case 'applicationFiled':
|
|
314
|
+
esc.filed.push(d.application as Application);
|
|
315
|
+
break;
|
|
316
|
+
case 'applicationSettled': {
|
|
317
|
+
const hash = d.applicationHash.toString();
|
|
318
|
+
esc.settled.push(hash);
|
|
319
|
+
view.settlements.set(hash, { subject: d.subject.toString(), outcome: Number(d.outcome.toString()), kept: d.kept.toString(), refunded: d.refunded.toString() });
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
// Payout and platform-key changes touch no permit (the account state has them).
|
|
323
|
+
case 'payoutProposed':
|
|
324
|
+
case 'payoutChanged':
|
|
325
|
+
case 'platformChanged':
|
|
326
|
+
break;
|
|
327
|
+
case 'registryRetired':
|
|
328
|
+
view.retiredRoot = d.permitsRoot.toString();
|
|
329
|
+
view.retiredByPlatform = d.byPlatform.toBoolean();
|
|
330
|
+
view.retiredForced = d.forced.toBoolean();
|
|
331
|
+
break;
|
|
332
|
+
default:
|
|
333
|
+
throw new Error(`unknown registry event type: ${e.type}`);
|
|
334
|
+
}
|
|
335
|
+
view.eventsApplied++;
|
|
336
|
+
}
|
|
337
|
+
// A rotation to epoch N implies the registryCreated keys were replaced.
|
|
338
|
+
const out = new Map<string, { type: string; leaf: PermitLeaf; rec: PermitRecord }>();
|
|
339
|
+
for (const [subject, list] of bySubject) {
|
|
340
|
+
list.sort((a, b) => a.rec.seq - b.rec.seq);
|
|
341
|
+
// Each change is the previous seq + 1, from the start: 0, or the seq a
|
|
342
|
+
// carried-over permit had when the registry started.
|
|
343
|
+
let prevSeq = view.permits.get(subject)?.seq ?? view.start?.seqs.get(subject) ?? 0;
|
|
344
|
+
for (const item of list) {
|
|
345
|
+
if (item.rec.seq !== prevSeq + 1) {
|
|
346
|
+
throw new Error(`event out of order for ${subject}: seq ${item.rec.seq} after ${prevSeq || 'none'}`);
|
|
347
|
+
}
|
|
348
|
+
prevSeq = item.rec.seq;
|
|
349
|
+
}
|
|
350
|
+
out.set(subject, list[list.length - 1]);
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Fetches a registry's events (active Mina instance) and rebuilds its tree. */
|
|
356
|
+
export async function rebuildRegistry(address: PublicKey | string): Promise<RegistryView> {
|
|
357
|
+
const pk = typeof address === 'string' ? PublicKey.fromBase58(address) : address;
|
|
358
|
+
const events = await new PermitRegistryEvents(pk).fetchEvents();
|
|
359
|
+
return applyRegistryEvents(emptyRegistryView(), events as RegistryEvent[]);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** 'moved': the holder moved it to a new key (a new permit, under a new subject); this one is closed. */
|
|
363
|
+
export type PermitStatusName = 'active' | 'suspended' | 'revoked' | 'moved' | 'expired' | 'not-yet-valid' | 'in-grace' | 'unknown';
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Effective status of a permit at `slot`: revoked and suspended win, then the
|
|
367
|
+
* dates. `gracePeriod` comes from the registry policy.
|
|
368
|
+
*/
|
|
369
|
+
/**
|
|
370
|
+
* A permit's status at `slot`. With the registry's `forkActivationSlot`
|
|
371
|
+
* (a successor deployed after a Mina hard fork): a permit that lapsed
|
|
372
|
+
* shortly before the fork, while its registry was retired and couldn't
|
|
373
|
+
* renew it, counts as expiring at the fork slot, so its grace runs from the
|
|
374
|
+
* fork (as licensing's effectiveExpirySlot). Anonymous holder proofs check
|
|
375
|
+
* the expiry as granted.
|
|
376
|
+
*/
|
|
377
|
+
export function permitStatusAt(rec: PermitRecord | undefined, slot: number, gracePeriod = 0, forkActivationSlot = 0): PermitStatusName {
|
|
378
|
+
if (!rec) return 'unknown';
|
|
379
|
+
if (rec.status === 2) return 'revoked';
|
|
380
|
+
if (rec.status === 3) return 'moved';
|
|
381
|
+
if (rec.status === 1) return 'suspended';
|
|
382
|
+
if (slot < rec.validFrom) return 'not-yet-valid';
|
|
383
|
+
const expiry = forkBumpedExpiry(rec.expirySlot, forkActivationSlot, gracePeriod);
|
|
384
|
+
if (slot <= expiry) return 'active';
|
|
385
|
+
if (slot <= expiry + gracePeriod) return 'in-grace';
|
|
386
|
+
return 'expired';
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export { unpackFees, unpackPolicy };
|
|
390
|
+
|
|
391
|
+
/** A registry view saved without its tree's hashes recomputed (see CompactMerkleMap). */
|
|
392
|
+
export type StoredRegistryView = {
|
|
393
|
+
root: string;
|
|
394
|
+
permits: PermitRecord[];
|
|
395
|
+
tree: { indices: string; leaves: string; splits: string };
|
|
396
|
+
/** Open applications (absent in views saved before escrow). */
|
|
397
|
+
applications?: ApplicationRecord[];
|
|
398
|
+
/** A redeployed registry's start (RegistryView.start): its root and the carried-over seqs. */
|
|
399
|
+
start?: { root: string; seqs: Array<[string, number]> };
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
export function registryViewToStored(view: RegistryView): StoredRegistryView {
|
|
403
|
+
const d = view.tree.toCompact();
|
|
404
|
+
return {
|
|
405
|
+
root: view.root, permits: [...view.permits.values()], applications: [...view.applications.values()],
|
|
406
|
+
tree: { indices: encodeFields(d.indices), leaves: encodeFields(d.leaves), splits: encodeFields(d.splits) },
|
|
407
|
+
...(view.start ? { start: { root: view.start.root, seqs: [...view.start.seqs] } } : {}),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Loads a saved view without re-hashing; its keys, epoch and fees come back
|
|
413
|
+
* with the next refreshRegistryView. Throws if the copy is inconsistent.
|
|
414
|
+
*/
|
|
415
|
+
export function registryViewFromStored(s: StoredRegistryView): RegistryView {
|
|
416
|
+
const tree = CompactMerkleMap.fromCompact({ indices: decodeFields(s.tree.indices), leaves: decodeFields(s.tree.leaves), splits: decodeFields(s.tree.splits) }, s.root);
|
|
417
|
+
if (tree.size !== s.permits.length) throw new Error('saved registry view: the permits and the tree disagree');
|
|
418
|
+
const permits = new Map(s.permits.map(p => [p.subject, p] as const));
|
|
419
|
+
const view: RegistryView = { tree, root: s.root, permits, keysHash: null, epoch: 0, feesPacked: null, ...emptyEscrow(), eventsApplied: 0 };
|
|
420
|
+
setApplications(view, new Map((s.applications ?? []).map(a => [a.subject, a] as const)));
|
|
421
|
+
if (s.start) view.start = { root: s.start.root, seqs: new Map(s.start.seqs) };
|
|
422
|
+
return view;
|
|
423
|
+
}
|