@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.
@@ -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
- * Open a v2 massif blob for MMR node reads. Nodes below `firstIndex` resolve
29
- * through the ancestor peak stack; nodes above `lastIndex` are not present in
30
- * this blob and throw.
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 declare function openMassifNodeStore(massifBytes: Uint8Array): MassifNodeStore;
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;AASH,OAAO,EAIL,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAW5B,MAAM,MAAM,eAAe,GAAG;IAC5B,uEAAuE;IACvE,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,WAAW,EAAE,UAAU,GAAG,eAAe,CA8C5E;AAED,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;AA2ID,6EAA6E;AAC7E,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAczD"}
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 { decode as decodeCbor, encode as encodeCbor } from "cbor-x";
18
- import { calculateRoot, Massif, peakStackEnd, } from "@forestrie/merklelog";
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
- * Open a v2 massif blob for MMR node reads. Nodes below `firstIndex` resolve
29
- * through the ancestor peak stack; nodes above `lastIndex` are not present in
30
- * this blob and throw.
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 function openMassifNodeStore(massifBytes) {
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 = decodeCbor(checkpointBytes);
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(decodeCbor(receiptBytes)));
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
- const encoded = encodeCbor(value);
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 = decodeCbor(proofBstr);
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 { decode as decodeCbor } from "cbor-x";
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 = decodeCbor(bytes);
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 { decode as decodeCbor } from "cbor-x";
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 = decodeCbor(genesisCbor);
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":"AACA,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"}
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"}
@@ -1,4 +1,4 @@
1
- import { decode as decodeCbor, encode as encodeCbor } from "cbor-x";
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 = decodeCbor(bytes);
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 = decodeCbor(bytes);
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 new Uint8Array(encodeCbor(map));
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, peakMMRIndexes, } from "./build-receipt-offline.js";
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";
@@ -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,EACf,cAAc,GACf,MAAM,4BAA4B,CAAC;AACpC,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"}
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, peakMMRIndexes, } from "./build-receipt-offline.js";
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";
@@ -1,4 +1,4 @@
1
- import { decode as decodeCbor } from "cbor-x";
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 = decodeCbor(receiptBytes);
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.4.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
- "cbor-x": "^1.5.9",
32
- "@forestrie/encoding": "0.2.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 { decode as decodeCbor, encode as encodeCbor } from "cbor-x";
18
+ import {
19
+ decodeCborDeterministic,
20
+ encodeCborDeterministic,
21
+ } from "@forestrie/encoding";
19
22
  import {
20
23
  calculateRoot,
21
- Massif,
22
- peakStackEnd,
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
- * Open a v2 massif blob for MMR node reads. Nodes below `firstIndex` resolve
53
- * through the ancestor peak stack; nodes above `lastIndex` are not present in
54
- * this blob and throw.
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 function openMassifNodeStore(massifBytes: Uint8Array): MassifNodeStore {
57
- const massif = new Massif(massifBytes);
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 = decodeCbor(checkpointBytes) as unknown;
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(decodeCbor(receiptBytes) as unknown),
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
- const encoded = encodeCbor(value);
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 = decodeCbor(proofBstr) as unknown;
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 { decode as decodeCbor } from "cbor-x";
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 = decodeCbor(bytes) as unknown;
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 { decode as decodeCbor } from "cbor-x";
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 = decodeCbor(genesisCbor);
48
+ raw = decodeCborDeterministic(genesisCbor);
49
49
  } catch {
50
50
  throw new Error("genesis CBOR decode failed");
51
51
  }
@@ -1,4 +1,7 @@
1
- import { decode as decodeCbor, encode as encodeCbor } from "cbor-x";
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 = decodeCbor(bytes) as unknown;
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 = decodeCbor(bytes) as unknown;
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 new Uint8Array(encodeCbor(map));
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";
@@ -1,4 +1,4 @@
1
- import { decode as decodeCbor } from "cbor-x";
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 = decodeCbor(receiptBytes) as unknown;
65
+ const decoded = decodeCborDeterministic(receiptBytes);
66
66
  const unwrapped = unwrapCoseSign1Tag(decoded);
67
67
  requireCoseSign1(unwrapped);
68
68