@forestrie/receipt-verify 1.0.0 → 2.0.0

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.
Files changed (52) hide show
  1. package/dist/attach-transparent-statement-receipt.d.ts +5 -1
  2. package/dist/attach-transparent-statement-receipt.d.ts.map +1 -1
  3. package/dist/attach-transparent-statement-receipt.js +7 -3
  4. package/dist/build-receipt-offline.d.ts +8 -1
  5. package/dist/build-receipt-offline.d.ts.map +1 -1
  6. package/dist/build-receipt-offline.js +31 -24
  7. package/dist/chain-binding.d.ts +19 -0
  8. package/dist/chain-binding.d.ts.map +1 -0
  9. package/dist/chain-binding.js +1 -0
  10. package/dist/checkpoint-chain.d.ts +130 -17
  11. package/dist/checkpoint-chain.d.ts.map +1 -1
  12. package/dist/checkpoint-chain.js +216 -117
  13. package/dist/decode-chain-binding-from-genesis.d.ts +15 -0
  14. package/dist/decode-chain-binding-from-genesis.d.ts.map +1 -0
  15. package/dist/decode-chain-binding-from-genesis.js +55 -0
  16. package/dist/decode-checkpoint-consistency-proof.d.ts +38 -0
  17. package/dist/decode-checkpoint-consistency-proof.d.ts.map +1 -0
  18. package/dist/decode-checkpoint-consistency-proof.js +108 -0
  19. package/dist/decode-genesis-cbor-map.d.ts +12 -0
  20. package/dist/decode-genesis-cbor-map.d.ts.map +1 -0
  21. package/dist/decode-genesis-cbor-map.js +26 -0
  22. package/dist/decode-trust-root-from-genesis.d.ts +21 -3
  23. package/dist/decode-trust-root-from-genesis.d.ts.map +1 -1
  24. package/dist/decode-trust-root-from-genesis.js +32 -23
  25. package/dist/decoded-trust-root.d.ts +17 -0
  26. package/dist/decoded-trust-root.d.ts.map +1 -0
  27. package/dist/decoded-trust-root.js +1 -0
  28. package/dist/forest-genesis-labels.d.ts +7 -0
  29. package/dist/forest-genesis-labels.d.ts.map +1 -1
  30. package/dist/forest-genesis-labels.js +7 -0
  31. package/dist/freshen-receipt.d.ts +23 -6
  32. package/dist/freshen-receipt.d.ts.map +1 -1
  33. package/dist/freshen-receipt.js +29 -17
  34. package/dist/index.d.ts +22 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +20 -2
  37. package/dist/parse-receipt.d.ts.map +1 -1
  38. package/dist/parse-receipt.js +2 -3
  39. package/package.json +4 -4
  40. package/src/attach-transparent-statement-receipt.ts +10 -3
  41. package/src/build-receipt-offline.ts +41 -22
  42. package/src/chain-binding.ts +18 -0
  43. package/src/checkpoint-chain.ts +279 -136
  44. package/src/decode-chain-binding-from-genesis.ts +77 -0
  45. package/src/decode-checkpoint-consistency-proof.ts +136 -0
  46. package/src/decode-genesis-cbor-map.ts +27 -0
  47. package/src/decode-trust-root-from-genesis.ts +40 -25
  48. package/src/decoded-trust-root.ts +17 -0
  49. package/src/forest-genesis-labels.ts +7 -0
  50. package/src/freshen-receipt.ts +56 -25
  51. package/src/index.ts +33 -1
  52. package/src/parse-receipt.ts +2 -4
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Decode the draft-bryce consistency proof `[tree-size-1, tree-size-2, paths,
3
+ * right-peaks]` carried under a checkpoint's verifiable-proofs UNPROTECTED
4
+ * header (draft-bryce label 396, key -2 = `VDP_CONSISTENCY_PROOF_KEY`).
5
+ *
6
+ * Single source of truth for this decode, shared by `parseCheckpoint`
7
+ * (build-receipt-offline.ts, lenient: an absent or malformed proof yields a
8
+ * `null` sealed size rather than a throw) and `checkpointConsistencyProof`
9
+ * (checkpoint-chain.ts, full validation: an absent proof or a malformed
10
+ * shape throws, and the SIGNED `tree-size-2` from the protected header —
11
+ * read separately via `readProtectedTreeSize2`, ADR-0066 D1 as amended —
12
+ * must match the `tree-size-2` decoded here; `tree-size-1` is unsigned
13
+ * prover context and is not cross-checked against a signed value).
14
+ */
15
+
16
+ import {
17
+ COSE_LABEL_VDP,
18
+ VDP_CONSISTENCY_PROOF_KEY,
19
+ decodeCborDeterministic,
20
+ } from "@forestrie/encoding";
21
+
22
+ /** The declared (unprotected, unsigned) consistency proof of a checkpoint. */
23
+ export type DecodedConsistencyProof = {
24
+ treeSize1: bigint;
25
+ treeSize2: bigint;
26
+ /** One inclusion path per tree-size-1 peak, proven at tree-size-2. */
27
+ paths: Uint8Array[][];
28
+ /** New peaks not covered by the proven roots (draft `right-peaks`). */
29
+ rightPeaks: Uint8Array[];
30
+ };
31
+
32
+ function asBigint(v: unknown, what: string): bigint {
33
+ // Unsigned only: a negative size flows into peakMMRIndexes /
34
+ // consistentRootsForSizes, which reject or spin on a non-positive
35
+ // argument — a malformed `.sth` must be rejected in bounded time, before
36
+ // any such call (FOR-414).
37
+ if (typeof v === "bigint") {
38
+ if (v < 0n) {
39
+ throw new Error(`${what}: must be an unsigned integer, got ${v}`);
40
+ }
41
+ return v;
42
+ }
43
+ if (typeof v === "number" && Number.isSafeInteger(v) && v >= 0) {
44
+ return BigInt(v);
45
+ }
46
+ throw new Error(`${what}: expected an unsigned integer`);
47
+ }
48
+
49
+ function asBytesArray(v: unknown, what: string): Uint8Array[] {
50
+ if (
51
+ !Array.isArray(v) ||
52
+ v.some((e) => !(e instanceof Uint8Array) || e.length !== 32)
53
+ ) {
54
+ throw new Error(`${what}: expected an array of 32-byte strings`);
55
+ }
56
+ return v as Uint8Array[];
57
+ }
58
+
59
+ /**
60
+ * Decode the embedded consistency proof from a checkpoint's UNPROTECTED
61
+ * header map. Returns `null` when the checkpoint carries no verifiable-proofs
62
+ * header (396) or no consistency-proof bstr there (key -2) — an ABSENT
63
+ * proof, not a malformed one.
64
+ *
65
+ * @throws When a consistency-proof bstr IS present but its contents are not
66
+ * the shape `[tree-size-1, tree-size-2, paths, right-peaks]`, either size
67
+ * is not an unsigned integer, the proof does not grow the tree
68
+ * (`tree-size-2 <= tree-size-1`), a path element or a right-peak is not a
69
+ * 32-byte string — or when header 396 is present but is not map-valued,
70
+ * or its `-2` entry is present but not a byte string.
71
+ */
72
+ export function decodeConsistencyProofFromUnprotected(
73
+ unprotected: Map<number, unknown>,
74
+ ): DecodedConsistencyProof | null {
75
+ const vdpRaw = unprotected.get(COSE_LABEL_VDP);
76
+ if (vdpRaw === undefined || vdpRaw === null) return null;
77
+ if (!(vdpRaw instanceof Map)) {
78
+ throw new Error("checkpoint carries no verifiable-proofs header (396)");
79
+ }
80
+ const proofBstr = vdpRaw.get(VDP_CONSISTENCY_PROOF_KEY);
81
+ if (proofBstr === undefined || proofBstr === null) return null;
82
+ if (!(proofBstr instanceof Uint8Array)) {
83
+ throw new Error("checkpoint carries no consistency proof (vdp key -2)");
84
+ }
85
+ const proof = decodeCborDeterministic(proofBstr);
86
+ if (!Array.isArray(proof) || proof.length < 4) {
87
+ throw new Error(
88
+ "consistency proof must be [tree-size-1, tree-size-2, paths, right-peaks]",
89
+ );
90
+ }
91
+ const pathsRaw = proof[2];
92
+ if (!Array.isArray(pathsRaw)) {
93
+ throw new Error("consistency paths must be arrays of 32-byte strings");
94
+ }
95
+ // Every path element is an MMR node, so it is 32 bytes — the same rule
96
+ // `asBytesArray` applies to right-peaks. Both reach the same places: the
97
+ // fold hashes them, and the peaks that come out are concatenated by
98
+ // `accumulatorPayload` with no length delimiter, so a node of any other
99
+ // length makes that payload ambiguous. Checking it here also bounds the
100
+ // work an unauthenticated `.sth` can ask for before its signature is
101
+ // consulted.
102
+ for (let i = 0; i < pathsRaw.length; i++) {
103
+ const path = pathsRaw[i] as unknown;
104
+ if (!Array.isArray(path)) {
105
+ throw new Error(
106
+ `consistency path ${i}: expected an array of 32-byte strings`,
107
+ );
108
+ }
109
+ for (let j = 0; j < path.length; j++) {
110
+ const node = path[j] as unknown;
111
+ if (!(node instanceof Uint8Array) || node.length !== 32) {
112
+ throw new Error(
113
+ `consistency path ${i} element ${j}: expected a 32-byte string`,
114
+ );
115
+ }
116
+ }
117
+ }
118
+ const treeSize1 = asBigint(proof[0], "tree-size-1");
119
+ const treeSize2 = asBigint(proof[1], "tree-size-2");
120
+ // A consistency proof strictly grows the tree; enforce `0 <= ts1 < ts2`
121
+ // (ts1 == 0 is a legitimate base-0 first link). This is the primary guard
122
+ // that keeps sizes non-negative and growing before they reach
123
+ // `consistentRootsForSizes` (FOR-414); the unsigned check in `asBigint`
124
+ // and `SizeMustIncrease` in `@forestrie/merklelog` are defence-in-depth.
125
+ if (treeSize2 <= treeSize1) {
126
+ throw new Error(
127
+ `consistency proof must grow the tree: tree-size-1 ${treeSize1} < tree-size-2 ${treeSize2}`,
128
+ );
129
+ }
130
+ return {
131
+ treeSize1,
132
+ treeSize2,
133
+ paths: pathsRaw as Uint8Array[][],
134
+ rightPeaks: asBytesArray(proof[3], "right-peaks"),
135
+ };
136
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Shared decode step for a genesis document's CBOR body: deterministic CBOR
3
+ * decodes an integer-keyed map either as a native `Map` or as a plain
4
+ * object (encoder-dependent), so callers that read genesis labels normalise
5
+ * to a `Map<number, unknown>` once here rather than each re-implementing
6
+ * the same fallback. Used by {@link decodeTrustRootFromGenesis} and
7
+ * {@link decodeChainBindingFromGenesis}.
8
+ */
9
+ export function decodeGenesisBodyAsIntKeyMap(
10
+ raw: unknown,
11
+ ): Map<number, unknown> | null {
12
+ if (raw instanceof Map) return raw as Map<number, unknown>;
13
+ if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
14
+ const out = new Map<number, unknown>();
15
+ for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
16
+ const n = Number(k);
17
+ if (Number.isFinite(n)) out.set(n, v);
18
+ }
19
+ return out;
20
+ }
21
+ return null;
22
+ }
23
+
24
+ /** Narrow a decoded genesis field to `Uint8Array`, or `null` if it is not one. */
25
+ export function asGenesisUint8Array(v: unknown): Uint8Array | null {
26
+ return v instanceof Uint8Array ? v : null;
27
+ }
@@ -17,39 +17,35 @@ import {
17
17
  FOREST_GENESIS_SCHEMA_V2,
18
18
  } from "./forest-genesis-labels.js";
19
19
  import { decodeTrustRootCbor } from "./decode-trust-root-cbor.js";
20
- import type { RootVerifyKey } from "./root-verify-key.js";
21
-
22
- function decodeBodyAsIntKeyMap(raw: unknown): Map<number, unknown> | null {
23
- if (raw instanceof Map) return raw as Map<number, unknown>;
24
- if (typeof raw === "object" && raw !== null && !Array.isArray(raw)) {
25
- const out = new Map<number, unknown>();
26
- for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
27
- const n = Number(k);
28
- if (Number.isFinite(n)) out.set(n, v);
29
- }
30
- return out;
31
- }
32
- return null;
33
- }
34
-
35
- function asGenesisUint8Array(v: unknown): Uint8Array | null {
36
- return v instanceof Uint8Array ? v : null;
37
- }
20
+ import { isParsedKs256RootKey, type RootVerifyKey } from "./root-verify-key.js";
21
+ import {
22
+ asGenesisUint8Array,
23
+ decodeGenesisBodyAsIntKeyMap,
24
+ } from "./decode-genesis-cbor-map.js";
25
+ import type { DecodedTrustRoot } from "./decoded-trust-root.js";
38
26
 
39
27
  /**
40
- * Extract receipt verify key from a forest genesis document CBOR blob.
41
- * Offline path: genesis-only trust anchor (ADR-0045).
28
+ * Extract the receipt verify key from a forest genesis document CBOR blob,
29
+ * alongside `bootstrapKeyXy` when the bootstrap key is ES256. Offline path:
30
+ * genesis-only trust anchor (ADR-0045). See {@link DecodedTrustRoot}.
31
+ * `bootstrapKeyXy` is read directly from the genesis-encoded bytes (never
32
+ * exported from the non-extractable `CryptoKey` in `key`), and is
33
+ * `undefined` for a KS256 v2 bootstrap key (an on-chain address — there is
34
+ * no P-256 public key to give up in that case).
35
+ *
36
+ * This is the single decode path: {@link decodeTrustRootFromGenesis} is
37
+ * `(await decodeTrustRootDetailsFromGenesis(genesisCbor)).key`.
42
38
  */
43
- export async function decodeTrustRootFromGenesis(
39
+ export async function decodeTrustRootDetailsFromGenesis(
44
40
  genesisCbor: Uint8Array,
45
- ): Promise<RootVerifyKey> {
41
+ ): Promise<DecodedTrustRoot> {
46
42
  let raw: unknown;
47
43
  try {
48
44
  raw = decodeCborDeterministic(genesisCbor);
49
45
  } catch {
50
46
  throw new Error("genesis CBOR decode failed");
51
47
  }
52
- const m = decodeBodyAsIntKeyMap(raw);
48
+ const m = decodeGenesisBodyAsIntKeyMap(raw);
53
49
  if (!m) throw new Error("genesis document must be a CBOR map");
54
50
 
55
51
  const versionRaw = m.get(FOREST_GENESIS_LABEL_GENESIS_VERSION);
@@ -61,7 +57,11 @@ export async function decodeTrustRootFromGenesis(
61
57
  if (bootstrapKey === null) {
62
58
  throw new Error("v2 genesis missing bootstrapKey");
63
59
  }
64
- return decodeTrustRootCbor({ alg, key: bootstrapKey });
60
+ const key = await decodeTrustRootCbor({ alg, key: bootstrapKey });
61
+ return {
62
+ key,
63
+ bootstrapKeyXy: isParsedKs256RootKey(key) ? undefined : bootstrapKey,
64
+ };
65
65
  }
66
66
 
67
67
  const kty = m.get(COSE_KEY_KTY);
@@ -76,7 +76,8 @@ export async function decodeTrustRootFromGenesis(
76
76
  const xy = new Uint8Array(64);
77
77
  xy.set(x, 0);
78
78
  xy.set(y, 32);
79
- return decodeTrustRootCbor({ alg: COSE_ALG_ES256, key: xy });
79
+ const key = await decodeTrustRootCbor({ alg: COSE_ALG_ES256, key: xy });
80
+ return { key, bootstrapKeyXy: xy };
80
81
  }
81
82
 
82
83
  if (versionRaw === FOREST_GENESIS_SCHEMA_V1 || versionRaw === undefined) {
@@ -85,3 +86,17 @@ export async function decodeTrustRootFromGenesis(
85
86
 
86
87
  throw new Error("unsupported genesis document");
87
88
  }
89
+
90
+ /**
91
+ * Extract the receipt verify key from a forest genesis document CBOR blob.
92
+ * Offline path: genesis-only trust anchor (ADR-0045). The origin/main
93
+ * signature and behaviour (including a KS256 v2 bootstrap key resolving to
94
+ * the `ParsedKs256RootKey` on-chain address, not a throw): callers pinned
95
+ * to `RootVerifyKey` keep working unchanged. For the raw bootstrap public
96
+ * key bytes as well, use {@link decodeTrustRootDetailsFromGenesis}.
97
+ */
98
+ export async function decodeTrustRootFromGenesis(
99
+ genesisCbor: Uint8Array,
100
+ ): Promise<RootVerifyKey> {
101
+ return (await decodeTrustRootDetailsFromGenesis(genesisCbor)).key;
102
+ }
@@ -0,0 +1,17 @@
1
+ import type { RootVerifyKey } from "./root-verify-key.js";
2
+
3
+ /**
4
+ * Result of {@link decodeTrustRootDetailsFromGenesis}: the decoded verify
5
+ * key (an ES256 `CryptoKey`, or a KS256 on-chain address) plus, for the
6
+ * ES256 case, the raw 64-byte x||y P-256 public key coordinates the genesis
7
+ * document carries. `bootstrapKeyXy` is read directly from the
8
+ * genesis-encoded bytes, not exported from `key` (`key` stays
9
+ * non-extractable) — plan-2609-07 L3, for callers that need the
10
+ * serialisable public key material the `CryptoKey` cannot give up.
11
+ * `bootstrapKeyXy` is `undefined` for a KS256 v2 bootstrap key (an on-chain
12
+ * address — there is no P-256 public key to give up).
13
+ */
14
+ export interface DecodedTrustRoot {
15
+ key: RootVerifyKey;
16
+ bootstrapKeyXy?: Uint8Array;
17
+ }
@@ -1,6 +1,13 @@
1
1
  export const FOREST_GENESIS_LABEL_GENESIS_VERSION = -68009;
2
2
  export const FOREST_GENESIS_LABEL_GENESIS_ALG = -68014;
3
3
  export const FOREST_GENESIS_LABEL_BOOTSTRAP_KEY = -68015;
4
+ /**
5
+ * The forest's own log id, wire-encoded as 32 bytes: 16 zero bytes then the
6
+ * 16-byte forest bootstrap log id (plan-2609-07 L3; mirrors mcp-resolve's
7
+ * `genesis-binding.ts` naming so that consumer can import this instead of
8
+ * defining it locally). See {@link decodeChainBindingFromGenesis}.
9
+ */
10
+ export const FOREST_GENESIS_LABEL_LOG_ID = -68010;
4
11
  export const FOREST_GENESIS_LABEL_UNIVOCITY_ADDR = -68011;
5
12
  export const FOREST_GENESIS_LABEL_CHAIN_ID = -68013;
6
13
  export const FOREST_GENESIS_SCHEMA_V2 = 2;
@@ -48,8 +48,10 @@
48
48
  import {
49
49
  calculateRoot,
50
50
  inclusionProofPath,
51
+ mmrSizeForLeafCount,
51
52
  peakIndexForLeafProof,
52
53
  peakMMRIndexes,
54
+ peaksBitmap,
53
55
  } from "@forestrie/merklelog";
54
56
  import {
55
57
  assembleReceiptFromProof,
@@ -62,19 +64,33 @@ import {
62
64
  import { parseReceipt } from "./parse-receipt.js";
63
65
  import { SubtleHasher } from "./subtle-hasher.js";
64
66
 
67
+ /**
68
+ * BREAKING (within the 2.0.0 major already in flight): the sizeless
69
+ * `accumulatorFrom?: Uint8Array[]` seed is replaced by `trustedBase`, which
70
+ * carries the seed's SIZE alongside its peaks. Without a size the fold ran
71
+ * each link against the size read off that link's own proof, which is what
72
+ * {@link computeCheckpointAccumulator} exists to prevent (ADR-0066 D5.4) —
73
+ * a seed could only ever be checked against the peak count the proof's own
74
+ * declared base implies, and many sizes share a peak count.
75
+ */
65
76
  export type FreshenReceiptInput = {
66
77
  /** The stale receipt (COSE Sign1 with a 396 inclusion proof). */
67
78
  oldReceiptBytes: Uint8Array;
68
79
  /** The leaf's committed value: `SHA-256(idtimestamp ‖ inner)` — the same
69
80
  * value `verify` recomputes from the entry (caller derives it). */
70
81
  leafValue: Uint8Array;
71
- /** Consistency-proof chain covering [0 or a trusted seed] → the latest sealed
72
- * size, in ascending contiguous order (the raw per-checkpoint proofs, with
73
- * `paths`). The chain's last link must end at the checkpoint's sealed size. */
82
+ /** Consistency-proof chain covering [0 or the trusted base] → the latest
83
+ * sealed size, in ascending contiguous order (the raw per-checkpoint
84
+ * proofs, with `paths`). The chain's last link must end at the
85
+ * checkpoint's sealed size. */
74
86
  consistencyProofs: readonly CheckpointConsistencyProof[];
75
- /** Trusted accumulator seed for a suffix chain; omit for a chain from base 0.
76
- * Its peak count must match the first link's tree-size-1. */
77
- accumulatorFrom?: Uint8Array[];
87
+ /** Trusted base for a suffix chain — the size the caller already trusts
88
+ * and that size's accumulator; omit for a chain from base 0 (size 0, an
89
+ * empty accumulator). The size must be a complete MMR size, the
90
+ * accumulator must hold one peak per peak of that size, and the first
91
+ * link's declared `tree-size-1` must equal the size. Same shape as
92
+ * `verifyCheckpointChain`'s `trustedBase`. */
93
+ trustedBase?: { size: bigint; accumulator: Uint8Array[] };
78
94
  /** The latest checkpoint (`.sth`): its pre-signed peak receipts + delegation
79
95
  * cert become the freshened receipt's signature. */
80
96
  latestCheckpointBytes: Uint8Array;
@@ -134,22 +150,32 @@ export async function freshenReceipt(
134
150
  );
135
151
  }
136
152
  const firstLink = links[0]!;
137
- // Base: a base-0 chain starts from an empty accumulator; a suffix chain's
138
- // trusted seed must have the peak count of its tree-size-1.
139
- const baseCount = input.accumulatorFrom?.length ?? 0;
140
- if (firstLink.treeSize1 === 0n) {
141
- if (baseCount !== 0) {
142
- throw new Error(
143
- "base-0 consistency chain must start from an empty accumulator seed",
144
- );
145
- }
146
- } else {
147
- const wanted = peakMMRIndexes(firstLink.treeSize1 - 1n).length;
148
- if (baseCount !== wanted) {
149
- throw new Error(
150
- `base accumulator has ${baseCount} peaks; first link size ${firstLink.treeSize1} requires ${wanted}`,
151
- );
152
- }
153
+ // Base: the CALLER's trusted size and its accumulator (size 0 and an empty
154
+ // accumulator for a whole-log chain). The size has to be a size an MMR can
155
+ // have — `peaksBitmap` rounds an incomplete one DOWN, so a size of 5 would
156
+ // fold the 4 -> N shape — and the accumulator has to hold that size's
157
+ // peaks, which is the check the proof's own declared base used to stand in
158
+ // for.
159
+ const baseSize = input.trustedBase?.size ?? 0n;
160
+ const baseAccumulator = input.trustedBase?.accumulator ?? [];
161
+ if (
162
+ baseSize < 0n ||
163
+ mmrSizeForLeafCount(peaksBitmap(baseSize)) !== baseSize
164
+ ) {
165
+ throw new Error(`trusted base size ${baseSize} is not a complete MMR size`);
166
+ }
167
+ const basePeaks = baseSize === 0n ? 0 : peakMMRIndexes(baseSize - 1n).length;
168
+ if (baseAccumulator.length !== basePeaks) {
169
+ throw new Error(
170
+ `base accumulator has ${baseAccumulator.length} peaks; trusted base size ${baseSize} has ${basePeaks}`,
171
+ );
172
+ }
173
+ // The first link must continue from that size, not from a size it names
174
+ // itself (ADR-0066 D5.4).
175
+ if (firstLink.treeSize1 !== baseSize) {
176
+ throw new Error(
177
+ `first consistency proof declares tree-size-1 ${firstLink.treeSize1}; the trusted base size is ${baseSize}`,
178
+ );
153
179
  }
154
180
  // Contiguity: each link continues where the previous one sealed.
155
181
  for (let i = 1; i < links.length; i++) {
@@ -167,10 +193,15 @@ export async function freshenReceipt(
167
193
  );
168
194
  }
169
195
 
170
- // Fold the chain to the latest accumulator (self-check target).
171
- let accumulator = input.accumulatorFrom ?? [];
196
+ // Fold the chain to the latest accumulator (self-check target). Each step
197
+ // runs against a size the caller trusts, never one read off the link being
198
+ // folded: the trusted base for the first link, and the size the previous
199
+ // link was just folded TO for every link after it.
200
+ let accumulator = baseAccumulator;
201
+ let sizeFrom = baseSize;
172
202
  for (const p of links) {
173
- accumulator = await computeCheckpointAccumulator(p, accumulator);
203
+ accumulator = await computeCheckpointAccumulator(p, accumulator, sizeFrom);
204
+ sizeFrom = p.treeSize2;
174
205
  }
175
206
  const aLatest = accumulator;
176
207
 
package/src/index.ts CHANGED
@@ -31,7 +31,37 @@ export type {
31
31
  * re-exported here to preserve the receipt-verify public surface.
32
32
  */
33
33
  export { peakMMRIndexes } from "@forestrie/merklelog";
34
- export { decodeTrustRootFromGenesis } from "./decode-trust-root-from-genesis.js";
34
+ /**
35
+ * `decodeTrustRootFromGenesis` keeps its origin/main signature and
36
+ * behaviour (`Promise<RootVerifyKey>`, including a KS256 v2 bootstrap key
37
+ * resolving without a throw) so pinned consumers (mcp-verify 1.0.0) are
38
+ * unaffected. `decodeTrustRootDetailsFromGenesis` is the additive sibling
39
+ * (plan-2609-07 L3) carrying `bootstrapKeyXy` alongside `key`.
40
+ */
41
+ export {
42
+ decodeTrustRootDetailsFromGenesis,
43
+ decodeTrustRootFromGenesis,
44
+ } from "./decode-trust-root-from-genesis.js";
45
+ export type { DecodedTrustRoot } from "./decoded-trust-root.js";
46
+ /**
47
+ * Genesis document label constants (plan-2609-07 L3): exported from the
48
+ * package root so consumers stop redefining them locally (mcp-resolve's own
49
+ * `genesis-binding.ts` did, before this).
50
+ */
51
+ export {
52
+ FOREST_GENESIS_LABEL_BOOTSTRAP_KEY,
53
+ FOREST_GENESIS_LABEL_CHAIN_ID,
54
+ FOREST_GENESIS_LABEL_GENESIS_ALG,
55
+ FOREST_GENESIS_LABEL_GENESIS_VERSION,
56
+ FOREST_GENESIS_LABEL_LOG_ID,
57
+ FOREST_GENESIS_LABEL_UNIVOCITY_ADDR,
58
+ } from "./forest-genesis-labels.js";
59
+ /**
60
+ * Genesis-bound chain binding (plan-2609-07 L3): mirrors mcp-resolve's own
61
+ * `decodeChainBindingFromGenesis` so that consumer can delete its copy.
62
+ */
63
+ export { decodeChainBindingFromGenesis } from "./decode-chain-binding-from-genesis.js";
64
+ export type { ChainBinding } from "./chain-binding.js";
35
65
  export { verifyGrantReceiptOffline } from "./verify-grant-receipt-offline.js";
36
66
  export { verifyReceiptOffline } from "./verify-grant-receipt-offline.js";
37
67
  /**
@@ -125,6 +155,8 @@ export {
125
155
  checkpointConsistencyProof,
126
156
  computeCheckpointAccumulator,
127
157
  verifyCheckpointChain,
158
+ CheckpointHighSSignatureError,
159
+ CheckpointSignedSizeMismatchError,
128
160
  type CheckpointChainLink,
129
161
  type CheckpointChainResult,
130
162
  type CheckpointConsistencyProof,
@@ -1,8 +1,6 @@
1
- import { decodeCborDeterministic } from "@forestrie/encoding";
1
+ import { COSE_LABEL_VDP, decodeCborDeterministic } from "@forestrie/encoding";
2
2
  import type { Proof } from "@forestrie/merklelog";
3
3
 
4
- const VDS_COSE_RECEIPT_PROOFS_TAG = 396;
5
-
6
4
  export type CoseSign1 = [
7
5
  protectedHeader: Uint8Array,
8
6
  unprotectedHeader: Map<number, unknown> | Record<string, unknown>,
@@ -81,7 +79,7 @@ export function parseReceipt(receiptBytes: Uint8Array): {
81
79
  }
82
80
 
83
81
  const unprotected = toHeaderMap(coseSign1[1]);
84
- const proofsRaw = unprotected.get(VDS_COSE_RECEIPT_PROOFS_TAG);
82
+ const proofsRaw = unprotected.get(COSE_LABEL_VDP);
85
83
  if (!proofsRaw || typeof proofsRaw !== "object") {
86
84
  throw new Error("Receipt missing header 396 (inclusion proof)");
87
85
  }