@forestrie/receipt-verify 2.1.0 → 3.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.
@@ -58,6 +58,7 @@ import {
58
58
  parseCheckpoint,
59
59
  } from "./build-receipt-offline.js";
60
60
  import {
61
+ CheckpointSignedSizeMismatchError,
61
62
  computeCheckpointAccumulator,
62
63
  type CheckpointConsistencyProof,
63
64
  } from "./checkpoint-chain.js";
@@ -80,9 +81,13 @@ export type FreshenReceiptInput = {
80
81
  * value `verify` recomputes from the entry (caller derives it). */
81
82
  leafValue: Uint8Array;
82
83
  /** 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. */
84
+ * sealed size, in ascending contiguous order: one entry per checkpoint,
85
+ * each holding the proofs that checkpoint relays (`proofs`, one or more —
86
+ * ADR-0066 D2). Every link's `signedTreeSize2` must equal its
87
+ * `treeSize2` — the size its LAST relayed proof reaches — as
88
+ * `checkpointConsistencyProof` requires of the checkpoint it decoded each
89
+ * link from. The chain's last link must end at the checkpoint's sealed
90
+ * size. */
86
91
  consistencyProofs: readonly CheckpointConsistencyProof[];
87
92
  /** Trusted base for a suffix chain — the size the caller already trusts
88
93
  * and that size's accumulator; omit for a chain from base 0 (size 0, an
@@ -111,9 +116,11 @@ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
111
116
  }
112
117
 
113
118
  /**
114
- * Freshen a stale receipt to the latest sealed state. Throws if the chain is
115
- * not a contiguous cover from the trusted base to the checkpoint's sealed size,
116
- * if a climb node is missing from the supplied proofs, or if the recomputed
119
+ * Freshen a stale receipt to the latest sealed state. Throws if a link's
120
+ * signed `tree-size-2` disagrees with its declared one
121
+ * ({@link CheckpointSignedSizeMismatchError}), if the chain is not a
122
+ * contiguous cover from the trusted base to the checkpoint's sealed size, if
123
+ * a climb node is missing from the supplied proofs, or if the recomputed
117
124
  * peak does not match the folded latest accumulator.
118
125
  */
119
126
  export async function freshenReceipt(
@@ -170,6 +177,20 @@ export async function freshenReceipt(
170
177
  `base accumulator has ${baseAccumulator.length} peaks; trusted base size ${baseSize} has ${basePeaks}`,
171
178
  );
172
179
  }
180
+ // Every link's SIGNED tree-size-2 must equal its declared one — the same
181
+ // equality `checkpointConsistencyProof` enforces when it decodes a
182
+ // checkpoint (ADR-0066 D1 as amended, D5.5). These links arrive already
183
+ // decoded, so nothing here had re-read the field the type says was pinned
184
+ // to the signature, and a link carrying `signedTreeSize2: 999n` beside
185
+ // `treeSize2: 7n` freshened (review finding I5).
186
+ for (let i = 0; i < links.length; i++) {
187
+ const link = links[i]!;
188
+ if (link.signedTreeSize2 !== link.treeSize2) {
189
+ throw new CheckpointSignedSizeMismatchError(
190
+ `consistency proof ${i}: signed tree-size-2 (-65933) ${link.signedTreeSize2} != declared consistency-proof tree-size-2 ${link.treeSize2}`,
191
+ );
192
+ }
193
+ }
173
194
  // The first link must continue from that size, not from a size it names
174
195
  // itself (ADR-0066 D5.4).
175
196
  if (firstLink.treeSize1 !== baseSize) {
@@ -177,7 +198,9 @@ export async function freshenReceipt(
177
198
  `first consistency proof declares tree-size-1 ${firstLink.treeSize1}; the trusted base size is ${baseSize}`,
178
199
  );
179
200
  }
180
- // Contiguity: each link continues where the previous one sealed.
201
+ // Contiguity BETWEEN checkpoints: each link continues where the previous
202
+ // one sealed. Contiguity WITHIN a link — between the proofs one
203
+ // checkpoint relays — is `computeCheckpointAccumulator`'s, below.
181
204
  for (let i = 1; i < links.length; i++) {
182
205
  if (links[i]!.treeSize1 !== links[i - 1]!.treeSize2) {
183
206
  throw new Error(
@@ -193,10 +216,11 @@ export async function freshenReceipt(
193
216
  );
194
217
  }
195
218
 
196
- // Fold the chain to the latest accumulator (self-check target). Each step
219
+ // Fold the chain to the latest accumulator (self-check target). Each link
197
220
  // runs against a size the caller trusts, never one read off the link being
198
221
  // folded: the trusted base for the first link, and the size the previous
199
- // link was just folded TO for every link after it.
222
+ // link was just folded TO for every link after it. A link relaying several
223
+ // proofs folds them all, in order, and ends at its last one.
200
224
  let accumulator = baseAccumulator;
201
225
  let sizeFrom = baseSize;
202
226
  for (const p of links) {
@@ -240,18 +264,21 @@ export async function freshenReceipt(
240
264
  for (let k = 0; k < oldPath.length; k++) {
241
265
  store.set(fullIndices[k]!, oldPath[k]!);
242
266
  }
243
- for (const link of links) {
244
- // A base-0 link (treeSize1 === 0) has no from-peaks to climb — a 0→N
267
+ // Every relayed proof contributes, not just one per checkpoint: a link
268
+ // that relays several sealed steps (ADR-0066 D2) carries one set of paths
269
+ // per step, each addressed by that step's own two sizes.
270
+ for (const step of links.flatMap((link) => link.proofs)) {
271
+ // A base-0 step (treeSize1 === 0) has no from-peaks to climb — a 0→N
245
272
  // consistency proof carries `paths: []` (the whole accumulator is its
246
273
  // right-peaks). Skip it: it contributes no store nodes, and calling
247
274
  // `peakMMRIndexes(-1n)` would throw (`posHeight(0)`, FOR-414). A genesis-
248
- // rooted `.sth` chain always starts with such a link.
249
- if (link.treeSize1 === 0n) continue;
250
- const fromPeaks = peakMMRIndexes(link.treeSize1 - 1n);
275
+ // rooted `.sth` chain always starts with such a step.
276
+ if (step.treeSize1 === 0n) continue;
277
+ const fromPeaks = peakMMRIndexes(step.treeSize1 - 1n);
251
278
  fromPeaks.forEach((peakIndex, j) => {
252
- const climb = link.paths[j];
279
+ const climb = step.paths[j];
253
280
  if (climb === undefined) return;
254
- const climbIndices = inclusionProofPath(link.treeSize2 - 1n, peakIndex);
281
+ const climbIndices = inclusionProofPath(step.treeSize2 - 1n, peakIndex);
255
282
  climbIndices.forEach((ix, e) => {
256
283
  const v = climb[e];
257
284
  if (v !== undefined) store.set(ix, v);
package/src/index.ts CHANGED
@@ -156,11 +156,17 @@ export {
156
156
  computeCheckpointAccumulator,
157
157
  verifyCheckpointChain,
158
158
  CheckpointHighSSignatureError,
159
+ CheckpointProtectedHeaderAlgError,
159
160
  CheckpointSignedSizeMismatchError,
161
+ ConsistencyChainNotContiguousError,
162
+ EmptyConsistencyProofsError,
160
163
  type CheckpointChainLink,
161
164
  type CheckpointChainResult,
162
165
  type CheckpointConsistencyProof,
163
166
  } from "./checkpoint-chain.js";
167
+ /** One relayed consistency proof; `CheckpointConsistencyProof.proofs` holds
168
+ * the chain of them a checkpoint carries (ADR-0066 D2). */
169
+ export type { DecodedConsistencyProof } from "./decode-checkpoint-consistency-proof.js";
164
170
  /**
165
171
  * Univocity leaf commitment hash. Was CLI-private (forestrie-cli's own
166
172
  * mirror, "hoist to the library when the FOR-297 multi-hop resolver lands");