@forestrie/receipt-verify 0.4.0 → 0.5.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/dist/build-receipt-offline.d.ts +6 -15
- package/dist/build-receipt-offline.d.ts.map +1 -1
- package/dist/build-receipt-offline.js +10 -201
- package/dist/decode-forestrie-grant-cose.js +2 -2
- package/dist/decode-trust-root-from-genesis.js +2 -2
- package/dist/grant-codec.d.ts.map +1 -1
- package/dist/grant-codec.js +4 -4
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/parse-receipt.js +2 -2
- package/package.json +3 -4
- package/src/build-receipt-offline.ts +18 -245
- package/src/decode-forestrie-grant-cose.ts +2 -2
- package/src/decode-trust-root-from-genesis.ts +2 -2
- package/src/grant-codec.ts +7 -4
- package/src/index.ts +5 -1
- package/src/parse-receipt.ts +2 -2
|
@@ -14,22 +14,15 @@
|
|
|
14
14
|
* at an externally attested tree size (e.g. Univocity `logState` on Base
|
|
15
15
|
* Sepolia) and compare the computed peak with the published accumulator.
|
|
16
16
|
*/
|
|
17
|
+
import { openMassifNodeStore, type MassifNodeStore } from "@forestrie/merklelog";
|
|
17
18
|
import { type CoseSign1 } from "./parse-receipt.js";
|
|
18
|
-
export type MassifNodeStore = {
|
|
19
|
-
/** 32-byte node at MMR index `i` (log data or ancestor peak stack). */
|
|
20
|
-
get(i: bigint): Uint8Array;
|
|
21
|
-
massifHeight: number;
|
|
22
|
-
massifIndex: bigint;
|
|
23
|
-
firstIndex: bigint;
|
|
24
|
-
/** Last MMR index with log data in this massif blob. */
|
|
25
|
-
lastIndex: bigint;
|
|
26
|
-
};
|
|
27
19
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
20
|
+
* Re-exported for compatibility. The MMR proof math and massif node store now
|
|
21
|
+
* live in `@forestrie/merklelog` (plan-2607-15 §4, phase 2); receipt-verify
|
|
22
|
+
* keeps only the COSE-shaped layer below.
|
|
31
23
|
*/
|
|
32
|
-
export
|
|
24
|
+
export { openMassifNodeStore };
|
|
25
|
+
export type { MassifNodeStore };
|
|
33
26
|
export type ParsedCheckpoint = {
|
|
34
27
|
coseSign1: CoseSign1;
|
|
35
28
|
unprotected: Map<number, unknown>;
|
|
@@ -73,6 +66,4 @@ export declare function computeAccumulatorPeak(opts: {
|
|
|
73
66
|
mmrIndex: bigint;
|
|
74
67
|
mmrSize: bigint;
|
|
75
68
|
}): Promise<ComputedAccumulatorPeak>;
|
|
76
|
-
/** MMR indices of the peaks of the tree ending at `mmrIndex` (inclusive). */
|
|
77
|
-
export declare function peakMMRIndexes(mmrIndex: bigint): bigint[];
|
|
78
69
|
//# sourceMappingURL=build-receipt-offline.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-receipt-offline.d.ts","sourceRoot":"","sources":["../src/build-receipt-offline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"build-receipt-offline.d.ts","sourceRoot":"","sources":["../src/build-receipt-offline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAMH,OAAO,EAIL,mBAAmB,EAEnB,KAAK,eAAe,EAErB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAQ5B;;;;GAIG;AACH,OAAO,EAAE,mBAAmB,EAAE,CAAC;AAC/B,YAAY,EAAE,eAAe,EAAE,CAAC;AAEhC,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,oEAAoE;IACpE,YAAY,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC/B,wEAAwE;IACxE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,cAAc,EAAE,UAAU,GAAG,IAAI,CAAC;CACnC,CAAC;AAEF,wDAAwD;AACxD,wBAAgB,eAAe,CAAC,eAAe,EAAE,UAAU,GAAG,gBAAgB,CAqB7E;AAED,MAAM,MAAM,wBAAwB,GAAG;IACrC,WAAW,EAAE,UAAU,CAAC;IACxB,eAAe,EAAE,UAAU,CAAC;IAC5B,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,wBAAwB,GAC9B,UAAU,CA2EZ;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,mEAAmE;IACnE,IAAI,EAAE,UAAU,CAAC;IACjB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,SAAS,EAAE,UAAU,CAAC;CACvB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE;IACjD,WAAW,EAAE,UAAU,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAwBnC"}
|
|
@@ -14,63 +14,23 @@
|
|
|
14
14
|
* at an externally attested tree size (e.g. Univocity `logState` on Base
|
|
15
15
|
* Sepolia) and compare the computed peak with the published accumulator.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
18
|
-
import { calculateRoot,
|
|
17
|
+
import { decodeCborDeterministic, encodeCborDeterministic, } from "@forestrie/encoding";
|
|
18
|
+
import { calculateRoot, inclusionProof, massifIndexFromMMRIndex, openMassifNodeStore, peakIndexForLeafProof, } from "@forestrie/merklelog";
|
|
19
19
|
import { requireCoseSign1, toHeaderMap, unwrapCoseSign1Tag, } from "./parse-receipt.js";
|
|
20
20
|
import { SubtleHasher } from "./subtle-hasher.js";
|
|
21
21
|
const VDS_COSE_RECEIPT_PROOFS_TAG = 396;
|
|
22
22
|
const SEAL_PEAK_RECEIPTS_LABEL = -65931;
|
|
23
23
|
const DELEGATION_CERT_LABEL = 1000;
|
|
24
24
|
const VDP_CONSISTENCY_PROOF_KEY = -2;
|
|
25
|
-
const VALUE_BYTES = 32n;
|
|
26
|
-
const MAX_MMR_HEIGHT = 64n;
|
|
27
25
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
26
|
+
* Re-exported for compatibility. The MMR proof math and massif node store now
|
|
27
|
+
* live in `@forestrie/merklelog` (plan-2607-15 §4, phase 2); receipt-verify
|
|
28
|
+
* keeps only the COSE-shaped layer below.
|
|
31
29
|
*/
|
|
32
|
-
export
|
|
33
|
-
const massif = new Massif(massifBytes);
|
|
34
|
-
const start = massif.getStart();
|
|
35
|
-
const massifHeight = start.massifHeight;
|
|
36
|
-
if (!Number.isInteger(massifHeight) ||
|
|
37
|
-
massifHeight < 1 ||
|
|
38
|
-
massifHeight > 64) {
|
|
39
|
-
throw new Error(`massif header has invalid height ${massifHeight}`);
|
|
40
|
-
}
|
|
41
|
-
const massifIndex = BigInt(start.massifIndex);
|
|
42
|
-
const firstIndex = start.firstIndex;
|
|
43
|
-
const logStart = peakStackEnd(massifHeight);
|
|
44
|
-
const peakStackStart = logStart - MAX_MMR_HEIGHT * VALUE_BYTES;
|
|
45
|
-
const blobLen = BigInt(massifBytes.byteLength);
|
|
46
|
-
if (blobLen < logStart) {
|
|
47
|
-
throw new Error("massif blob too short for v2 layout");
|
|
48
|
-
}
|
|
49
|
-
const logNodeCount = (blobLen - logStart) / VALUE_BYTES;
|
|
50
|
-
const lastIndex = firstIndex + logNodeCount - 1n;
|
|
51
|
-
const stackMap = peakStackMapForMassif(massifHeight, firstIndex);
|
|
52
|
-
const get = (i) => {
|
|
53
|
-
if (i >= firstIndex) {
|
|
54
|
-
if (i > lastIndex) {
|
|
55
|
-
throw new Error(`mmr index ${i.toString(10)} is beyond this massif's log data ` +
|
|
56
|
-
`(last ${lastIndex.toString(10)}); local content does not cover ` +
|
|
57
|
-
`the requested tree size`);
|
|
58
|
-
}
|
|
59
|
-
const off = logStart + (i - firstIndex) * VALUE_BYTES;
|
|
60
|
-
return slice32(massifBytes, off, "log-data");
|
|
61
|
-
}
|
|
62
|
-
const peakIdx = stackMap.get(i);
|
|
63
|
-
if (peakIdx === undefined) {
|
|
64
|
-
throw new Error(`missing ancestor peak for mmr index ${i.toString(10)}`);
|
|
65
|
-
}
|
|
66
|
-
const off = peakStackStart + BigInt(peakIdx) * VALUE_BYTES;
|
|
67
|
-
return slice32(massifBytes, off, "peak-stack");
|
|
68
|
-
};
|
|
69
|
-
return { get, massifHeight, massifIndex, firstIndex, lastIndex };
|
|
70
|
-
}
|
|
30
|
+
export { openMassifNodeStore };
|
|
71
31
|
/** Parse a format-v3 checkpoint (`.sth`) COSE Sign1. */
|
|
72
32
|
export function parseCheckpoint(checkpointBytes) {
|
|
73
|
-
const decoded =
|
|
33
|
+
const decoded = decodeCborDeterministic(checkpointBytes);
|
|
74
34
|
const coseSign1 = requireCoseSign1(unwrapCoseSign1Tag(decoded));
|
|
75
35
|
const unprotected = toHeaderMap(coseSign1[1]);
|
|
76
36
|
const peakReceiptsRaw = unprotected.get(SEAL_PEAK_RECEIPTS_LABEL);
|
|
@@ -124,7 +84,7 @@ export function buildReceiptOffline(input) {
|
|
|
124
84
|
if (!(receiptBytes instanceof Uint8Array)) {
|
|
125
85
|
throw new Error(`checkpoint peak receipt slot ${peakIdx} is missing or not bstr`);
|
|
126
86
|
}
|
|
127
|
-
const receiptSign1 = requireCoseSign1(unwrapCoseSign1Tag(
|
|
87
|
+
const receiptSign1 = requireCoseSign1(unwrapCoseSign1Tag(decodeCborDeterministic(receiptBytes)));
|
|
128
88
|
const receiptUnprotected = toHeaderMap(receiptSign1[1]);
|
|
129
89
|
if (checkpoint.delegationCert) {
|
|
130
90
|
receiptUnprotected.set(DELEGATION_CERT_LABEL, checkpoint.delegationCert);
|
|
@@ -174,17 +134,7 @@ export async function computeAccumulatorPeak(opts) {
|
|
|
174
134
|
}
|
|
175
135
|
// --- helpers ---
|
|
176
136
|
function cborBytes(value) {
|
|
177
|
-
|
|
178
|
-
return encoded instanceof Uint8Array
|
|
179
|
-
? encoded
|
|
180
|
-
: new Uint8Array(encoded);
|
|
181
|
-
}
|
|
182
|
-
function slice32(buf, offset, label) {
|
|
183
|
-
if (offset < 0n || offset + 32n > BigInt(buf.byteLength)) {
|
|
184
|
-
throw new Error(`out of range read for ${label}: off=${offset.toString(10)}`);
|
|
185
|
-
}
|
|
186
|
-
const start = Number(offset);
|
|
187
|
-
return buf.slice(start, start + 32);
|
|
137
|
+
return encodeCborDeterministic(value);
|
|
188
138
|
}
|
|
189
139
|
/**
|
|
190
140
|
* Sealed mmr size from a format-v3 checkpoint: tree-size-2 of the consistency
|
|
@@ -199,7 +149,7 @@ function sealedSizeFromCheckpoint(unprotected) {
|
|
|
199
149
|
const proofBstr = vdp.get(VDP_CONSISTENCY_PROOF_KEY);
|
|
200
150
|
if (!(proofBstr instanceof Uint8Array))
|
|
201
151
|
return null;
|
|
202
|
-
const proof =
|
|
152
|
+
const proof = decodeCborDeterministic(proofBstr);
|
|
203
153
|
if (!Array.isArray(proof) || proof.length < 2)
|
|
204
154
|
return null;
|
|
205
155
|
const treeSize2 = proof[1];
|
|
@@ -210,144 +160,3 @@ function sealedSizeFromCheckpoint(unprotected) {
|
|
|
210
160
|
}
|
|
211
161
|
return null;
|
|
212
162
|
}
|
|
213
|
-
// --- MMR index math (ported from go-merklelog/mmr, mirrors resolve-receipt) ---
|
|
214
|
-
function bitLength(num) {
|
|
215
|
-
if (num === 0n)
|
|
216
|
-
return 0;
|
|
217
|
-
return num.toString(2).length;
|
|
218
|
-
}
|
|
219
|
-
function allOnes(num) {
|
|
220
|
-
return num > 0n && (num & (num + 1n)) === 0n;
|
|
221
|
-
}
|
|
222
|
-
function jumpLeftPerfect(pos) {
|
|
223
|
-
const bl = bitLength(pos);
|
|
224
|
-
if (bl === 0)
|
|
225
|
-
return pos;
|
|
226
|
-
const msb = 1n << BigInt(bl - 1);
|
|
227
|
-
return pos - (msb - 1n);
|
|
228
|
-
}
|
|
229
|
-
function posHeight(pos) {
|
|
230
|
-
let current = pos;
|
|
231
|
-
while (!allOnes(current)) {
|
|
232
|
-
current = jumpLeftPerfect(current);
|
|
233
|
-
}
|
|
234
|
-
return bitLength(current) - 1;
|
|
235
|
-
}
|
|
236
|
-
function indexHeight(i) {
|
|
237
|
-
return posHeight(i + 1n);
|
|
238
|
-
}
|
|
239
|
-
function inclusionProof(getNode, mmrLastIndex, i) {
|
|
240
|
-
if (i > mmrLastIndex) {
|
|
241
|
-
throw new Error("index out of range");
|
|
242
|
-
}
|
|
243
|
-
let g = BigInt(indexHeight(i));
|
|
244
|
-
const proof = [];
|
|
245
|
-
while (true) {
|
|
246
|
-
const siblingOffset = 2n << g;
|
|
247
|
-
let iSibling;
|
|
248
|
-
if (BigInt(indexHeight(i + 1n)) > g) {
|
|
249
|
-
iSibling = i - siblingOffset + 1n;
|
|
250
|
-
i += 1n;
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
iSibling = i + siblingOffset - 1n;
|
|
254
|
-
i += siblingOffset;
|
|
255
|
-
}
|
|
256
|
-
if (iSibling > mmrLastIndex) {
|
|
257
|
-
return proof;
|
|
258
|
-
}
|
|
259
|
-
proof.push(getNode(iSibling));
|
|
260
|
-
g += 1n;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
function peaksBitmap(mmrSize) {
|
|
264
|
-
if (mmrSize === 0n)
|
|
265
|
-
return 0n;
|
|
266
|
-
let pos = mmrSize;
|
|
267
|
-
let peakSize = (1n << BigInt(bitLength(mmrSize))) - 1n;
|
|
268
|
-
let peakMap = 0n;
|
|
269
|
-
while (peakSize > 0n) {
|
|
270
|
-
peakMap <<= 1n;
|
|
271
|
-
if (pos >= peakSize) {
|
|
272
|
-
pos -= peakSize;
|
|
273
|
-
peakMap |= 1n;
|
|
274
|
-
}
|
|
275
|
-
peakSize >>= 1n;
|
|
276
|
-
}
|
|
277
|
-
return peakMap;
|
|
278
|
-
}
|
|
279
|
-
function popcount64(x) {
|
|
280
|
-
let count = 0;
|
|
281
|
-
let v = x;
|
|
282
|
-
while (v > 0n) {
|
|
283
|
-
if ((v & 1n) === 1n)
|
|
284
|
-
count += 1;
|
|
285
|
-
v >>= 1n;
|
|
286
|
-
}
|
|
287
|
-
return count;
|
|
288
|
-
}
|
|
289
|
-
function peakIndexForLeafProof(mmrSize, proofLen) {
|
|
290
|
-
const leafCount = peaksBitmap(mmrSize);
|
|
291
|
-
const peaksMask = (1n << BigInt(proofLen + 1)) - 1n;
|
|
292
|
-
return popcount64(leafCount) - popcount64(leafCount & peaksMask);
|
|
293
|
-
}
|
|
294
|
-
function topPeak(i) {
|
|
295
|
-
const bl = bitLength(i + 2n);
|
|
296
|
-
return (1n << BigInt(bl - 1)) - 2n;
|
|
297
|
-
}
|
|
298
|
-
/** MMR indices of the peaks of the tree ending at `mmrIndex` (inclusive). */
|
|
299
|
-
export function peakMMRIndexes(mmrIndex) {
|
|
300
|
-
let mmrSize = mmrIndex + 1n;
|
|
301
|
-
if (posHeight(mmrSize + 1n) > posHeight(mmrSize)) {
|
|
302
|
-
return [];
|
|
303
|
-
}
|
|
304
|
-
let peak = 0n;
|
|
305
|
-
const out = [];
|
|
306
|
-
while (mmrSize !== 0n) {
|
|
307
|
-
const peakSize = topPeak(mmrSize - 1n) + 1n;
|
|
308
|
-
peak = peak + peakSize;
|
|
309
|
-
out.push(peak - 1n);
|
|
310
|
-
mmrSize -= peakSize;
|
|
311
|
-
}
|
|
312
|
-
return out;
|
|
313
|
-
}
|
|
314
|
-
function peakStackMapForMassif(massifHeight, firstIndex) {
|
|
315
|
-
const map = new Map();
|
|
316
|
-
const iPeaks = peakMMRIndexes(firstIndex);
|
|
317
|
-
for (let i = 0; i < iPeaks.length; i++) {
|
|
318
|
-
const ip = iPeaks[i];
|
|
319
|
-
if (indexHeight(ip) < massifHeight - 1) {
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
map.set(ip, i);
|
|
323
|
-
}
|
|
324
|
-
return map;
|
|
325
|
-
}
|
|
326
|
-
function mmrIndexFromLeafIndex(leafIndex) {
|
|
327
|
-
let sum = 0n;
|
|
328
|
-
let current = leafIndex;
|
|
329
|
-
while (current > 0n) {
|
|
330
|
-
const h = BigInt(bitLength(current));
|
|
331
|
-
sum += (1n << h) - 1n;
|
|
332
|
-
const half = 1n << (h - 1n);
|
|
333
|
-
current -= half;
|
|
334
|
-
}
|
|
335
|
-
return sum;
|
|
336
|
-
}
|
|
337
|
-
function firstMMRSize(mmrIndex) {
|
|
338
|
-
let i = mmrIndex;
|
|
339
|
-
let h0 = indexHeight(i);
|
|
340
|
-
let h1 = indexHeight(i + 1n);
|
|
341
|
-
while (h0 < h1) {
|
|
342
|
-
i += 1n;
|
|
343
|
-
h0 = h1;
|
|
344
|
-
h1 = indexHeight(i + 1n);
|
|
345
|
-
}
|
|
346
|
-
return i + 1n;
|
|
347
|
-
}
|
|
348
|
-
function massifIndexFromMMRIndex(massifHeight, mmrIndex) {
|
|
349
|
-
const size = firstMMRSize(mmrIndex);
|
|
350
|
-
const leafIndex = peaksBitmap(size) - 1n;
|
|
351
|
-
const massifMaxLeaves = 1n << BigInt(massifHeight - 1);
|
|
352
|
-
return leafIndex / massifMaxLeaves;
|
|
353
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sha256 } from "@noble/hashes/sha256";
|
|
2
|
-
import {
|
|
2
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
3
3
|
import { HEADER_FORESTRIE_GRANT_V0, HEADER_IDTIMESTAMP, } from "./forest-genesis-labels.js";
|
|
4
4
|
import { decodeGrantPayload } from "./grant-codec.js";
|
|
5
5
|
const IDTIMESTAMP_BYTES = 8;
|
|
@@ -31,7 +31,7 @@ function digestEquals(a, b) {
|
|
|
31
31
|
* Decode Forestrie-Grant COSE Sign1 (Custodian transparent statement profile).
|
|
32
32
|
*/
|
|
33
33
|
export function decodeForestrieGrantCose(bytes) {
|
|
34
|
-
const raw =
|
|
34
|
+
const raw = decodeCborDeterministic(bytes);
|
|
35
35
|
const arr = Array.isArray(raw) ? raw : null;
|
|
36
36
|
if (!arr || arr.length !== 4) {
|
|
37
37
|
throw new Error("Forestrie-Grant must be COSE Sign1 (array of 4)");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
2
2
|
import { COSE_ALG_ES256, COSE_CRV_P256, COSE_EC2_CRV, COSE_EC2_X, COSE_EC2_Y, COSE_KEY_ALG, COSE_KEY_KTY, COSE_KTY_EC2, } from "./cose-key.js";
|
|
3
3
|
import { FOREST_GENESIS_LABEL_BOOTSTRAP_KEY, FOREST_GENESIS_LABEL_GENESIS_ALG, FOREST_GENESIS_LABEL_GENESIS_VERSION, FOREST_GENESIS_SCHEMA_V1, FOREST_GENESIS_SCHEMA_V2, } from "./forest-genesis-labels.js";
|
|
4
4
|
import { decodeTrustRootCbor } from "./decode-trust-root-cbor.js";
|
|
@@ -26,7 +26,7 @@ function asGenesisUint8Array(v) {
|
|
|
26
26
|
export async function decodeTrustRootFromGenesis(genesisCbor) {
|
|
27
27
|
let raw;
|
|
28
28
|
try {
|
|
29
|
-
raw =
|
|
29
|
+
raw = decodeCborDeterministic(genesisCbor);
|
|
30
30
|
}
|
|
31
31
|
catch {
|
|
32
32
|
throw new Error("genesis CBOR decode failed");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grant-codec.d.ts","sourceRoot":"","sources":["../src/grant-codec.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"grant-codec.d.ts","sourceRoot":"","sources":["../src/grant-codec.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAuDjD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,KAAK,CAO3D;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG;IACtD,KAAK,EAAE,KAAK,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;CACzB,CAsBA;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,GAAG,UAAU,CAa3D"}
|
package/dist/grant-codec.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeCborDeterministic, encodeCborDeterministic, } from "@forestrie/encoding";
|
|
2
2
|
import { grantDataToBytes } from "@forestrie/encoding";
|
|
3
3
|
import { fromPaddedWire32, toPaddedWire32 } from "./uuid-bytes.js";
|
|
4
4
|
const CBOR_KEY_IDTIMESTAMP = 0;
|
|
@@ -48,14 +48,14 @@ function mapToGrant(m) {
|
|
|
48
48
|
export function decodeGrantPayload(bytes) {
|
|
49
49
|
if (!bytes?.length)
|
|
50
50
|
throw new Error("Grant payload is empty");
|
|
51
|
-
const raw =
|
|
51
|
+
const raw = decodeCborDeterministic(bytes);
|
|
52
52
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
53
53
|
throw new Error("Grant payload must be a CBOR map");
|
|
54
54
|
}
|
|
55
55
|
return mapToGrant(raw);
|
|
56
56
|
}
|
|
57
57
|
export function decodeGrantResponse(bytes) {
|
|
58
|
-
const raw =
|
|
58
|
+
const raw = decodeCborDeterministic(bytes);
|
|
59
59
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
60
60
|
throw new Error("Grant response must be a CBOR map");
|
|
61
61
|
}
|
|
@@ -84,5 +84,5 @@ export function encodeGrantPayload(grant) {
|
|
|
84
84
|
grantDataToBytes(grant.grantData ?? new Uint8Array(0)),
|
|
85
85
|
],
|
|
86
86
|
]);
|
|
87
|
-
return
|
|
87
|
+
return encodeCborDeterministic(map);
|
|
88
88
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,12 @@ export type { Grant } from "@forestrie/encoding";
|
|
|
4
4
|
export type { VerifyGrantReceiptOfflineInput } from "./verify-grant-receipt-offline.js";
|
|
5
5
|
export { parseReceipt } from "./parse-receipt.js";
|
|
6
6
|
export type { BuildReceiptOfflineInput, ComputedAccumulatorPeak, MassifNodeStore, ParsedCheckpoint, } from "./build-receipt-offline.js";
|
|
7
|
-
export { buildReceiptOffline, computeAccumulatorPeak, openMassifNodeStore, parseCheckpoint,
|
|
7
|
+
export { buildReceiptOffline, computeAccumulatorPeak, openMassifNodeStore, parseCheckpoint, } from "./build-receipt-offline.js";
|
|
8
|
+
/**
|
|
9
|
+
* MMR proof math now lives in @forestrie/merklelog (plan-2607-15 §4 hoist);
|
|
10
|
+
* re-exported here to preserve the receipt-verify public surface.
|
|
11
|
+
*/
|
|
12
|
+
export { peakMMRIndexes } from "@forestrie/merklelog";
|
|
8
13
|
export { decodeTrustRootFromGenesis } from "./decode-trust-root-from-genesis.js";
|
|
9
14
|
export { verifyGrantReceiptOffline } from "./verify-grant-receipt-offline.js";
|
|
10
15
|
export { decodeForestrieGrantCose } from "./decode-forestrie-grant-cose.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,oFAAoF;AACpF,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,YAAY,EAAE,8BAA8B,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,eAAe,EACf,gBAAgB,GACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,oFAAoF;AACpF,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,YAAY,EAAE,8BAA8B,EAAE,MAAM,mCAAmC,CAAC;AACxF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,eAAe,EACf,gBAAgB,GACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC;;;GAGG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,0EAA0E;AAC1E,OAAO,EACL,cAAc,EACd,iDAAiD,GAClD,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC7E,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export { parseReceipt } from "./parse-receipt.js";
|
|
2
|
-
export { buildReceiptOffline, computeAccumulatorPeak, openMassifNodeStore, parseCheckpoint,
|
|
2
|
+
export { buildReceiptOffline, computeAccumulatorPeak, openMassifNodeStore, parseCheckpoint, } from "./build-receipt-offline.js";
|
|
3
|
+
/**
|
|
4
|
+
* MMR proof math now lives in @forestrie/merklelog (plan-2607-15 §4 hoist);
|
|
5
|
+
* re-exported here to preserve the receipt-verify public surface.
|
|
6
|
+
*/
|
|
7
|
+
export { peakMMRIndexes } from "@forestrie/merklelog";
|
|
3
8
|
export { decodeTrustRootFromGenesis } from "./decode-trust-root-from-genesis.js";
|
|
4
9
|
export { verifyGrantReceiptOffline } from "./verify-grant-receipt-offline.js";
|
|
5
10
|
export { decodeForestrieGrantCose } from "./decode-forestrie-grant-cose.js";
|
package/dist/parse-receipt.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
2
2
|
const VDS_COSE_RECEIPT_PROOFS_TAG = 396;
|
|
3
3
|
export function unwrapCoseSign1Tag(value) {
|
|
4
4
|
if (value && typeof value === "object" && !(value instanceof Map)) {
|
|
@@ -42,7 +42,7 @@ export function toHeaderMap(value) {
|
|
|
42
42
|
return out;
|
|
43
43
|
}
|
|
44
44
|
export function parseReceipt(receiptBytes) {
|
|
45
|
-
const decoded =
|
|
45
|
+
const decoded = decodeCborDeterministic(receiptBytes);
|
|
46
46
|
const unwrapped = unwrapCoseSign1Tag(decoded);
|
|
47
47
|
requireCoseSign1(unwrapped);
|
|
48
48
|
const coseSign1 = requireCoseSign1(unwrapped);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forestrie/receipt-verify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Offline SCITT grant receipt verification (ADR-0045)",
|
|
@@ -28,9 +28,8 @@
|
|
|
28
28
|
],
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@noble/hashes": "^1.7.1",
|
|
31
|
-
"
|
|
32
|
-
"@forestrie/encoding": "0.
|
|
33
|
-
"@forestrie/merklelog": "0.0.3"
|
|
31
|
+
"@forestrie/merklelog": "0.1.0",
|
|
32
|
+
"@forestrie/encoding": "0.3.0"
|
|
34
33
|
},
|
|
35
34
|
"devDependencies": {
|
|
36
35
|
"esbuild": "^0.24.0",
|
|
@@ -15,11 +15,17 @@
|
|
|
15
15
|
* Sepolia) and compare the computed peak with the published accumulator.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
decodeCborDeterministic,
|
|
20
|
+
encodeCborDeterministic,
|
|
21
|
+
} from "@forestrie/encoding";
|
|
19
22
|
import {
|
|
20
23
|
calculateRoot,
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
inclusionProof,
|
|
25
|
+
massifIndexFromMMRIndex,
|
|
26
|
+
openMassifNodeStore,
|
|
27
|
+
peakIndexForLeafProof,
|
|
28
|
+
type MassifNodeStore,
|
|
23
29
|
type Proof,
|
|
24
30
|
} from "@forestrie/merklelog";
|
|
25
31
|
import {
|
|
@@ -35,71 +41,13 @@ const SEAL_PEAK_RECEIPTS_LABEL = -65931;
|
|
|
35
41
|
const DELEGATION_CERT_LABEL = 1000;
|
|
36
42
|
const VDP_CONSISTENCY_PROOF_KEY = -2;
|
|
37
43
|
|
|
38
|
-
const VALUE_BYTES = 32n;
|
|
39
|
-
const MAX_MMR_HEIGHT = 64n;
|
|
40
|
-
|
|
41
|
-
export type MassifNodeStore = {
|
|
42
|
-
/** 32-byte node at MMR index `i` (log data or ancestor peak stack). */
|
|
43
|
-
get(i: bigint): Uint8Array;
|
|
44
|
-
massifHeight: number;
|
|
45
|
-
massifIndex: bigint;
|
|
46
|
-
firstIndex: bigint;
|
|
47
|
-
/** Last MMR index with log data in this massif blob. */
|
|
48
|
-
lastIndex: bigint;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
44
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
45
|
+
* Re-exported for compatibility. The MMR proof math and massif node store now
|
|
46
|
+
* live in `@forestrie/merklelog` (plan-2607-15 §4, phase 2); receipt-verify
|
|
47
|
+
* keeps only the COSE-shaped layer below.
|
|
55
48
|
*/
|
|
56
|
-
export
|
|
57
|
-
|
|
58
|
-
const start = massif.getStart();
|
|
59
|
-
const massifHeight = start.massifHeight;
|
|
60
|
-
if (
|
|
61
|
-
!Number.isInteger(massifHeight) ||
|
|
62
|
-
massifHeight < 1 ||
|
|
63
|
-
massifHeight > 64
|
|
64
|
-
) {
|
|
65
|
-
throw new Error(`massif header has invalid height ${massifHeight}`);
|
|
66
|
-
}
|
|
67
|
-
const massifIndex = BigInt(start.massifIndex);
|
|
68
|
-
const firstIndex = start.firstIndex;
|
|
69
|
-
|
|
70
|
-
const logStart = peakStackEnd(massifHeight);
|
|
71
|
-
const peakStackStart = logStart - MAX_MMR_HEIGHT * VALUE_BYTES;
|
|
72
|
-
const blobLen = BigInt(massifBytes.byteLength);
|
|
73
|
-
if (blobLen < logStart) {
|
|
74
|
-
throw new Error("massif blob too short for v2 layout");
|
|
75
|
-
}
|
|
76
|
-
const logNodeCount = (blobLen - logStart) / VALUE_BYTES;
|
|
77
|
-
const lastIndex = firstIndex + logNodeCount - 1n;
|
|
78
|
-
|
|
79
|
-
const stackMap = peakStackMapForMassif(massifHeight, firstIndex);
|
|
80
|
-
|
|
81
|
-
const get = (i: bigint): Uint8Array => {
|
|
82
|
-
if (i >= firstIndex) {
|
|
83
|
-
if (i > lastIndex) {
|
|
84
|
-
throw new Error(
|
|
85
|
-
`mmr index ${i.toString(10)} is beyond this massif's log data ` +
|
|
86
|
-
`(last ${lastIndex.toString(10)}); local content does not cover ` +
|
|
87
|
-
`the requested tree size`,
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
const off = logStart + (i - firstIndex) * VALUE_BYTES;
|
|
91
|
-
return slice32(massifBytes, off, "log-data");
|
|
92
|
-
}
|
|
93
|
-
const peakIdx = stackMap.get(i);
|
|
94
|
-
if (peakIdx === undefined) {
|
|
95
|
-
throw new Error(`missing ancestor peak for mmr index ${i.toString(10)}`);
|
|
96
|
-
}
|
|
97
|
-
const off = peakStackStart + BigInt(peakIdx) * VALUE_BYTES;
|
|
98
|
-
return slice32(massifBytes, off, "peak-stack");
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
return { get, massifHeight, massifIndex, firstIndex, lastIndex };
|
|
102
|
-
}
|
|
49
|
+
export { openMassifNodeStore };
|
|
50
|
+
export type { MassifNodeStore };
|
|
103
51
|
|
|
104
52
|
export type ParsedCheckpoint = {
|
|
105
53
|
coseSign1: CoseSign1;
|
|
@@ -113,7 +61,7 @@ export type ParsedCheckpoint = {
|
|
|
113
61
|
|
|
114
62
|
/** Parse a format-v3 checkpoint (`.sth`) COSE Sign1. */
|
|
115
63
|
export function parseCheckpoint(checkpointBytes: Uint8Array): ParsedCheckpoint {
|
|
116
|
-
const decoded =
|
|
64
|
+
const decoded = decodeCborDeterministic(checkpointBytes);
|
|
117
65
|
const coseSign1 = requireCoseSign1(unwrapCoseSign1Tag(decoded));
|
|
118
66
|
const unprotected = toHeaderMap(coseSign1[1]);
|
|
119
67
|
|
|
@@ -198,7 +146,7 @@ export function buildReceiptOffline(
|
|
|
198
146
|
}
|
|
199
147
|
|
|
200
148
|
const receiptSign1 = requireCoseSign1(
|
|
201
|
-
unwrapCoseSign1Tag(
|
|
149
|
+
unwrapCoseSign1Tag(decodeCborDeterministic(receiptBytes)),
|
|
202
150
|
);
|
|
203
151
|
const receiptUnprotected = toHeaderMap(receiptSign1[1]);
|
|
204
152
|
|
|
@@ -275,20 +223,7 @@ export async function computeAccumulatorPeak(opts: {
|
|
|
275
223
|
// --- helpers ---
|
|
276
224
|
|
|
277
225
|
function cborBytes(value: unknown): Uint8Array {
|
|
278
|
-
|
|
279
|
-
return encoded instanceof Uint8Array
|
|
280
|
-
? encoded
|
|
281
|
-
: new Uint8Array(encoded as ArrayLike<number>);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function slice32(buf: Uint8Array, offset: bigint, label: string): Uint8Array {
|
|
285
|
-
if (offset < 0n || offset + 32n > BigInt(buf.byteLength)) {
|
|
286
|
-
throw new Error(
|
|
287
|
-
`out of range read for ${label}: off=${offset.toString(10)}`,
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
const start = Number(offset);
|
|
291
|
-
return buf.slice(start, start + 32);
|
|
226
|
+
return encodeCborDeterministic(value);
|
|
292
227
|
}
|
|
293
228
|
|
|
294
229
|
/**
|
|
@@ -304,7 +239,7 @@ function sealedSizeFromCheckpoint(
|
|
|
304
239
|
const vdp = toHeaderMap(vdpRaw as Map<number, unknown>);
|
|
305
240
|
const proofBstr = vdp.get(VDP_CONSISTENCY_PROOF_KEY);
|
|
306
241
|
if (!(proofBstr instanceof Uint8Array)) return null;
|
|
307
|
-
const proof =
|
|
242
|
+
const proof = decodeCborDeterministic(proofBstr);
|
|
308
243
|
if (!Array.isArray(proof) || proof.length < 2) return null;
|
|
309
244
|
const treeSize2 = proof[1];
|
|
310
245
|
if (typeof treeSize2 === "bigint") return treeSize2;
|
|
@@ -313,165 +248,3 @@ function sealedSizeFromCheckpoint(
|
|
|
313
248
|
}
|
|
314
249
|
return null;
|
|
315
250
|
}
|
|
316
|
-
|
|
317
|
-
// --- MMR index math (ported from go-merklelog/mmr, mirrors resolve-receipt) ---
|
|
318
|
-
|
|
319
|
-
function bitLength(num: bigint): number {
|
|
320
|
-
if (num === 0n) return 0;
|
|
321
|
-
return num.toString(2).length;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function allOnes(num: bigint): boolean {
|
|
325
|
-
return num > 0n && (num & (num + 1n)) === 0n;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function jumpLeftPerfect(pos: bigint): bigint {
|
|
329
|
-
const bl = bitLength(pos);
|
|
330
|
-
if (bl === 0) return pos;
|
|
331
|
-
const msb = 1n << BigInt(bl - 1);
|
|
332
|
-
return pos - (msb - 1n);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function posHeight(pos: bigint): number {
|
|
336
|
-
let current = pos;
|
|
337
|
-
while (!allOnes(current)) {
|
|
338
|
-
current = jumpLeftPerfect(current);
|
|
339
|
-
}
|
|
340
|
-
return bitLength(current) - 1;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
function indexHeight(i: bigint): number {
|
|
344
|
-
return posHeight(i + 1n);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function inclusionProof(
|
|
348
|
-
getNode: (i: bigint) => Uint8Array,
|
|
349
|
-
mmrLastIndex: bigint,
|
|
350
|
-
i: bigint,
|
|
351
|
-
): Uint8Array[] {
|
|
352
|
-
if (i > mmrLastIndex) {
|
|
353
|
-
throw new Error("index out of range");
|
|
354
|
-
}
|
|
355
|
-
let g = BigInt(indexHeight(i));
|
|
356
|
-
const proof: Uint8Array[] = [];
|
|
357
|
-
while (true) {
|
|
358
|
-
const siblingOffset = 2n << g;
|
|
359
|
-
let iSibling: bigint;
|
|
360
|
-
if (BigInt(indexHeight(i + 1n)) > g) {
|
|
361
|
-
iSibling = i - siblingOffset + 1n;
|
|
362
|
-
i += 1n;
|
|
363
|
-
} else {
|
|
364
|
-
iSibling = i + siblingOffset - 1n;
|
|
365
|
-
i += siblingOffset;
|
|
366
|
-
}
|
|
367
|
-
if (iSibling > mmrLastIndex) {
|
|
368
|
-
return proof;
|
|
369
|
-
}
|
|
370
|
-
proof.push(getNode(iSibling));
|
|
371
|
-
g += 1n;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
function peaksBitmap(mmrSize: bigint): bigint {
|
|
376
|
-
if (mmrSize === 0n) return 0n;
|
|
377
|
-
let pos = mmrSize;
|
|
378
|
-
let peakSize = (1n << BigInt(bitLength(mmrSize))) - 1n;
|
|
379
|
-
let peakMap = 0n;
|
|
380
|
-
while (peakSize > 0n) {
|
|
381
|
-
peakMap <<= 1n;
|
|
382
|
-
if (pos >= peakSize) {
|
|
383
|
-
pos -= peakSize;
|
|
384
|
-
peakMap |= 1n;
|
|
385
|
-
}
|
|
386
|
-
peakSize >>= 1n;
|
|
387
|
-
}
|
|
388
|
-
return peakMap;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
function popcount64(x: bigint): number {
|
|
392
|
-
let count = 0;
|
|
393
|
-
let v = x;
|
|
394
|
-
while (v > 0n) {
|
|
395
|
-
if ((v & 1n) === 1n) count += 1;
|
|
396
|
-
v >>= 1n;
|
|
397
|
-
}
|
|
398
|
-
return count;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
function peakIndexForLeafProof(mmrSize: bigint, proofLen: number): number {
|
|
402
|
-
const leafCount = peaksBitmap(mmrSize);
|
|
403
|
-
const peaksMask = (1n << BigInt(proofLen + 1)) - 1n;
|
|
404
|
-
return popcount64(leafCount) - popcount64(leafCount & peaksMask);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function topPeak(i: bigint): bigint {
|
|
408
|
-
const bl = bitLength(i + 2n);
|
|
409
|
-
return (1n << BigInt(bl - 1)) - 2n;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
/** MMR indices of the peaks of the tree ending at `mmrIndex` (inclusive). */
|
|
413
|
-
export function peakMMRIndexes(mmrIndex: bigint): bigint[] {
|
|
414
|
-
let mmrSize = mmrIndex + 1n;
|
|
415
|
-
if (posHeight(mmrSize + 1n) > posHeight(mmrSize)) {
|
|
416
|
-
return [];
|
|
417
|
-
}
|
|
418
|
-
let peak = 0n;
|
|
419
|
-
const out: bigint[] = [];
|
|
420
|
-
while (mmrSize !== 0n) {
|
|
421
|
-
const peakSize = topPeak(mmrSize - 1n) + 1n;
|
|
422
|
-
peak = peak + peakSize;
|
|
423
|
-
out.push(peak - 1n);
|
|
424
|
-
mmrSize -= peakSize;
|
|
425
|
-
}
|
|
426
|
-
return out;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function peakStackMapForMassif(
|
|
430
|
-
massifHeight: number,
|
|
431
|
-
firstIndex: bigint,
|
|
432
|
-
): Map<bigint, number> {
|
|
433
|
-
const map = new Map<bigint, number>();
|
|
434
|
-
const iPeaks = peakMMRIndexes(firstIndex);
|
|
435
|
-
for (let i = 0; i < iPeaks.length; i++) {
|
|
436
|
-
const ip = iPeaks[i]!;
|
|
437
|
-
if (indexHeight(ip) < massifHeight - 1) {
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
map.set(ip, i);
|
|
441
|
-
}
|
|
442
|
-
return map;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
function mmrIndexFromLeafIndex(leafIndex: bigint): bigint {
|
|
446
|
-
let sum = 0n;
|
|
447
|
-
let current = leafIndex;
|
|
448
|
-
while (current > 0n) {
|
|
449
|
-
const h = BigInt(bitLength(current));
|
|
450
|
-
sum += (1n << h) - 1n;
|
|
451
|
-
const half = 1n << (h - 1n);
|
|
452
|
-
current -= half;
|
|
453
|
-
}
|
|
454
|
-
return sum;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function firstMMRSize(mmrIndex: bigint): bigint {
|
|
458
|
-
let i = mmrIndex;
|
|
459
|
-
let h0 = indexHeight(i);
|
|
460
|
-
let h1 = indexHeight(i + 1n);
|
|
461
|
-
while (h0 < h1) {
|
|
462
|
-
i += 1n;
|
|
463
|
-
h0 = h1;
|
|
464
|
-
h1 = indexHeight(i + 1n);
|
|
465
|
-
}
|
|
466
|
-
return i + 1n;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function massifIndexFromMMRIndex(
|
|
470
|
-
massifHeight: number,
|
|
471
|
-
mmrIndex: bigint,
|
|
472
|
-
): bigint {
|
|
473
|
-
const size = firstMMRSize(mmrIndex);
|
|
474
|
-
const leafIndex = peaksBitmap(size) - 1n;
|
|
475
|
-
const massifMaxLeaves = 1n << BigInt(massifHeight - 1);
|
|
476
|
-
return leafIndex / massifMaxLeaves;
|
|
477
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sha256 } from "@noble/hashes/sha256";
|
|
2
|
-
import {
|
|
2
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
3
3
|
import type { Grant } from "@forestrie/encoding";
|
|
4
4
|
import {
|
|
5
5
|
HEADER_FORESTRIE_GRANT_V0,
|
|
@@ -42,7 +42,7 @@ export function decodeForestrieGrantCose(bytes: Uint8Array): {
|
|
|
42
42
|
grant: Grant;
|
|
43
43
|
idtimestampBe8: Uint8Array;
|
|
44
44
|
} {
|
|
45
|
-
const raw =
|
|
45
|
+
const raw = decodeCborDeterministic(bytes);
|
|
46
46
|
const arr = Array.isArray(raw) ? raw : null;
|
|
47
47
|
if (!arr || arr.length !== 4) {
|
|
48
48
|
throw new Error("Forestrie-Grant must be COSE Sign1 (array of 4)");
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
2
2
|
import {
|
|
3
3
|
COSE_ALG_ES256,
|
|
4
4
|
COSE_CRV_P256,
|
|
@@ -45,7 +45,7 @@ export async function decodeTrustRootFromGenesis(
|
|
|
45
45
|
): Promise<RootVerifyKey> {
|
|
46
46
|
let raw: unknown;
|
|
47
47
|
try {
|
|
48
|
-
raw =
|
|
48
|
+
raw = decodeCborDeterministic(genesisCbor);
|
|
49
49
|
} catch {
|
|
50
50
|
throw new Error("genesis CBOR decode failed");
|
|
51
51
|
}
|
package/src/grant-codec.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
decodeCborDeterministic,
|
|
3
|
+
encodeCborDeterministic,
|
|
4
|
+
} from "@forestrie/encoding";
|
|
2
5
|
import type { Grant } from "@forestrie/encoding";
|
|
3
6
|
import { grantDataToBytes } from "@forestrie/encoding";
|
|
4
7
|
import { fromPaddedWire32, toPaddedWire32 } from "./uuid-bytes.js";
|
|
@@ -56,7 +59,7 @@ function mapToGrant(m: Map<number, unknown> | Record<number, unknown>): Grant {
|
|
|
56
59
|
|
|
57
60
|
export function decodeGrantPayload(bytes: Uint8Array): Grant {
|
|
58
61
|
if (!bytes?.length) throw new Error("Grant payload is empty");
|
|
59
|
-
const raw =
|
|
62
|
+
const raw = decodeCborDeterministic(bytes);
|
|
60
63
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
61
64
|
throw new Error("Grant payload must be a CBOR map");
|
|
62
65
|
}
|
|
@@ -67,7 +70,7 @@ export function decodeGrantResponse(bytes: Uint8Array): {
|
|
|
67
70
|
grant: Grant;
|
|
68
71
|
idtimestamp: Uint8Array;
|
|
69
72
|
} {
|
|
70
|
-
const raw =
|
|
73
|
+
const raw = decodeCborDeterministic(bytes);
|
|
71
74
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
72
75
|
throw new Error("Grant response must be a CBOR map");
|
|
73
76
|
}
|
|
@@ -102,5 +105,5 @@ export function encodeGrantPayload(grant: Grant): Uint8Array {
|
|
|
102
105
|
grantDataToBytes(grant.grantData ?? new Uint8Array(0)),
|
|
103
106
|
],
|
|
104
107
|
]);
|
|
105
|
-
return
|
|
108
|
+
return encodeCborDeterministic(map);
|
|
106
109
|
}
|
package/src/index.ts
CHANGED
|
@@ -17,8 +17,12 @@ export {
|
|
|
17
17
|
computeAccumulatorPeak,
|
|
18
18
|
openMassifNodeStore,
|
|
19
19
|
parseCheckpoint,
|
|
20
|
-
peakMMRIndexes,
|
|
21
20
|
} from "./build-receipt-offline.js";
|
|
21
|
+
/**
|
|
22
|
+
* MMR proof math now lives in @forestrie/merklelog (plan-2607-15 §4 hoist);
|
|
23
|
+
* re-exported here to preserve the receipt-verify public surface.
|
|
24
|
+
*/
|
|
25
|
+
export { peakMMRIndexes } from "@forestrie/merklelog";
|
|
22
26
|
export { decodeTrustRootFromGenesis } from "./decode-trust-root-from-genesis.js";
|
|
23
27
|
export { verifyGrantReceiptOffline } from "./verify-grant-receipt-offline.js";
|
|
24
28
|
export { decodeForestrieGrantCose } from "./decode-forestrie-grant-cose.js";
|
package/src/parse-receipt.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { decodeCborDeterministic } from "@forestrie/encoding";
|
|
2
2
|
import type { Proof } from "@forestrie/merklelog";
|
|
3
3
|
|
|
4
4
|
const VDS_COSE_RECEIPT_PROOFS_TAG = 396;
|
|
@@ -62,7 +62,7 @@ export function parseReceipt(receiptBytes: Uint8Array): {
|
|
|
62
62
|
receiptCbor: Uint8Array;
|
|
63
63
|
coseSign1: CoseSign1;
|
|
64
64
|
} {
|
|
65
|
-
const decoded =
|
|
65
|
+
const decoded = decodeCborDeterministic(receiptBytes);
|
|
66
66
|
const unwrapped = unwrapCoseSign1Tag(decoded);
|
|
67
67
|
requireCoseSign1(unwrapped);
|
|
68
68
|
|