@forestrie/merklelog 0.3.0 → 0.5.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.
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Typed errors for the size-driven consistency fold
3
+ * (`consistentRootsForSizes`).
4
+ *
5
+ * The names mirror the Solidity `IUnivocityErrors` members raised by the
6
+ * on-chain implementation (`univocity/src/algorithms/consistentRoots.sol`),
7
+ * so a shape rejection reads the same on both sides. Every one of these
8
+ * reports a condition the two sizes fix in advance — the proof does not
9
+ * have the shape MMR(sizeFrom) -> MMR(sizeTo) implies — as distinct from a
10
+ * value mismatch against a trusted accumulator, which callers surface as a
11
+ * plain verification failure.
12
+ *
13
+ * All derive from {@link ConsistencyShapeError} so a caller can map the whole
14
+ * family to "malformed proof" with a single `instanceof`.
15
+ */
16
+ /** Base class for every consistency proof-shape rejection. */
17
+ export class ConsistencyShapeError extends Error {
18
+ constructor(message) {
19
+ super(message);
20
+ this.name = "ConsistencyShapeError";
21
+ }
22
+ }
23
+ /**
24
+ * The target size is not a complete MMR size.
25
+ *
26
+ * `peaksBitmap` rounds an incomplete size down to the largest complete MMR
27
+ * below it, so for an incomplete `sizeTo` the bitmap describes a different
28
+ * tree than the one named. Accepting it would anchor an accumulator that is
29
+ * no MMR's, and a later verifier would read its entries at the wrong heights.
30
+ */
31
+ export class IncompleteTreeSize extends ConsistencyShapeError {
32
+ size;
33
+ constructor(size) {
34
+ super(`size ${size} is not a complete MMR size`);
35
+ this.size = size;
36
+ this.name = "IncompleteTreeSize";
37
+ }
38
+ }
39
+ /**
40
+ * The accumulator or the proof list does not have one entry per origin peak.
41
+ * The origin peak count is fixed by `sizeFrom` alone.
42
+ */
43
+ export class ConsistencyPeakCountMismatch extends ConsistencyShapeError {
44
+ expected;
45
+ actual;
46
+ constructor(expected, actual) {
47
+ super(`expected ${expected} entries for the origin peaks, got ${actual}`);
48
+ this.expected = expected;
49
+ this.actual = actual;
50
+ this.name = "ConsistencyPeakCountMismatch";
51
+ }
52
+ }
53
+ /**
54
+ * A path does not have the length the two sizes imply: 0 for an origin peak
55
+ * above the split, `split - h` for an origin peak of height `h` below it.
56
+ */
57
+ export class ConsistencyPathLengthMismatch extends ConsistencyShapeError {
58
+ index;
59
+ expected;
60
+ actual;
61
+ constructor(index, expected, actual) {
62
+ super(`path ${index}: expected length ${expected}, got ${actual}`);
63
+ this.index = index;
64
+ this.expected = expected;
65
+ this.actual = actual;
66
+ this.name = "ConsistencyPathLengthMismatch";
67
+ }
68
+ }
69
+ /**
70
+ * Two origin peaks below the split proved different roots. Every origin peak
71
+ * below the split is committed by the single target peak of height `split`,
72
+ * so all of their paths must produce the same value.
73
+ */
74
+ export class ConsistencyRootMismatch extends ConsistencyShapeError {
75
+ index;
76
+ constructor(index) {
77
+ super(`path ${index} proves a different root from the paths before it`);
78
+ this.index = index;
79
+ this.name = "ConsistencyRootMismatch";
80
+ }
81
+ }
82
+ /**
83
+ * The target size does not exceed the origin size. Consistency is defined for
84
+ * a growing log; equal or decreasing sizes have no proof shape to check.
85
+ */
86
+ export class SizeMustIncrease extends ConsistencyShapeError {
87
+ sizeFrom;
88
+ sizeTo;
89
+ constructor(sizeFrom, sizeTo) {
90
+ super(`sizeTo (${sizeTo}) must exceed sizeFrom (${sizeFrom})`);
91
+ this.sizeFrom = sizeFrom;
92
+ this.sizeTo = sizeTo;
93
+ this.name = "SizeMustIncrease";
94
+ }
95
+ }
96
+ /**
97
+ * A size is outside the uint64 domain the MMR profile's sizes live in.
98
+ *
99
+ * Solidity and Go take these sizes as `uint64`, so the domain is part of
100
+ * their signatures; TypeScript's `bigint` has no bound of its own. Without
101
+ * this check a size at or above 2^64 folds here and nowhere else: the
102
+ * position prefix `hash_pospair64` writes is truncated to eight bytes, so
103
+ * the hashes computed above the bound are not the ones the other two
104
+ * implementations would compute, and a size that large is not one a CBOR
105
+ * unsigned integer can carry or the contract can store either.
106
+ */
107
+ export class SizeNotUint64 extends ConsistencyShapeError {
108
+ which;
109
+ size;
110
+ constructor(which, size) {
111
+ super(`${which} (${size}) is outside the uint64 range [0, 2^64 - 1]`);
112
+ this.which = which;
113
+ this.size = size;
114
+ this.name = "SizeNotUint64";
115
+ }
116
+ }
117
+ /**
118
+ * `paths` is not a dense array of `Uint8Array[]`.
119
+ *
120
+ * A hole (a sparse array), a `null`/`undefined` entry, a path that is not an
121
+ * array, or a path element that is not a `Uint8Array` all survive the peak
122
+ * COUNT check and then surface as a bare `TypeError` out of the fold, which
123
+ * escapes the {@link ConsistencyShapeError} family callers are told to
124
+ * switch on. The shape of the proof material is a proof-shape condition like
125
+ * any other, so it is reported as one.
126
+ */
127
+ export class ConsistencyPathMalformed extends ConsistencyShapeError {
128
+ detail;
129
+ index;
130
+ constructor(detail, index) {
131
+ super(index === undefined ? `paths: ${detail}` : `path ${index}: ${detail}`);
132
+ this.detail = detail;
133
+ this.index = index;
134
+ this.name = "ConsistencyPathMalformed";
135
+ }
136
+ }
@@ -11,6 +11,8 @@
11
11
  */
12
12
  /** Reads the 32-byte node at MMR index `i` from some backing store. */
13
13
  export type NodeGetter = (i: bigint) => Uint8Array;
14
+ /** Number of bits needed to represent `num` (0 for 0). */
15
+ export declare function bitLength(num: bigint): number;
14
16
  /**
15
17
  * Zero-based height index of the node at MMR index `i` (leaves are 0).
16
18
  * (go-merklelog `mmr/indexheight.go` IndexHeight)
@@ -68,6 +70,22 @@ export declare function inclusionProofLocalExtend(get: NodeGetter, mmrSizeA: big
68
70
  * subtree (peak). (go-merklelog `mmr/peaks.go` PeaksBitmap)
69
71
  */
70
72
  export declare function peaksBitmap(mmrSize: bigint): bigint;
73
+ /**
74
+ * Node count of the complete MMR with `leaves` leaves (reference
75
+ * `mmr_size_for_leaf_count`, Solidity `mmrSizeForLeafCount`).
76
+ *
77
+ * Every leaf adds itself plus one interior node per binary carry, and each
78
+ * peak is a carry that has not happened, so the count is
79
+ * `2 * leaves - popcount(leaves)`.
80
+ *
81
+ * Because {@link peaksBitmap} rounds an incomplete size down to the largest
82
+ * complete MMR below it, `mmrSizeForLeafCount(peaksBitmap(size)) === size`
83
+ * holds exactly for complete sizes; that identity is the completeness test
84
+ * used by `consistentRootsForSizes`.
85
+ */
86
+ export declare function mmrSizeForLeafCount(leaves: bigint): bigint;
87
+ /** Population count of a non-negative BigInt. */
88
+ export declare function popcount(x: bigint): number;
71
89
  /**
72
90
  * Accumulator slot (left-to-right) committed by an inclusion proof of length
73
91
  * `proofLen` in a tree of size `mmrSize`. (server resolve-receipt.ts:735,
@@ -1 +1 @@
1
- {"version":3,"file":"proof.d.ts","sourceRoot":"","sources":["../../src/mmr/proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,uEAAuE;AACvE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,UAAU,CAAC;AA2CnD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,UAAU,EACf,YAAY,EAAE,MAAM,EACpB,CAAC,EAAE,MAAM,GACR,UAAU,EAAE,CAyBd;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAmB5E;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,UAAU,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,CAAC,EAAE,MAAM,GACR,qBAAqB,CA4CvB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAcnD;AAaD;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,MAAM,CAIR;AAQD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAczD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,EACpB,CAAC,EAAE,MAAM,GACR,MAAM,CAKR;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GACjB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAWrB"}
1
+ {"version":3,"file":"proof.d.ts","sourceRoot":"","sources":["../../src/mmr/proof.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,uEAAuE;AACvE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,EAAE,MAAM,KAAK,UAAU,CAAC;AAEnD,0DAA0D;AAC1D,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAG7C;AAqCD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,UAAU,EACf,YAAY,EAAE,MAAM,EACpB,CAAC,EAAE,MAAM,GACR,UAAU,EAAE,CAyBd;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAmB5E;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,IAAI,EAAE,UAAU,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CACvC,GAAG,EAAE,UAAU,EACf,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,CAAC,EAAE,MAAM,GACR,qBAAqB,CA4CvB;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAcnD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,iDAAiD;AACjD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ1C;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,MAAM,CAIR;AAQD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAczD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,EACpB,CAAC,EAAE,MAAM,GACR,MAAM,CAKR;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAC1B,YAAY,EAAE,MAAM,EACpB,UAAU,EAAE,MAAM,GACjB,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAWrB"}
package/dist/mmr/proof.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * browser-safe (ADR-0048). Function names mirror the go-merklelog originals.
11
11
  */
12
12
  /** Number of bits needed to represent `num` (0 for 0). */
13
- function bitLength(num) {
13
+ export function bitLength(num) {
14
14
  if (num <= 0n)
15
15
  return 0;
16
16
  return num.toString(2).length;
@@ -193,8 +193,24 @@ export function peaksBitmap(mmrSize) {
193
193
  }
194
194
  return peakMap;
195
195
  }
196
+ /**
197
+ * Node count of the complete MMR with `leaves` leaves (reference
198
+ * `mmr_size_for_leaf_count`, Solidity `mmrSizeForLeafCount`).
199
+ *
200
+ * Every leaf adds itself plus one interior node per binary carry, and each
201
+ * peak is a carry that has not happened, so the count is
202
+ * `2 * leaves - popcount(leaves)`.
203
+ *
204
+ * Because {@link peaksBitmap} rounds an incomplete size down to the largest
205
+ * complete MMR below it, `mmrSizeForLeafCount(peaksBitmap(size)) === size`
206
+ * holds exactly for complete sizes; that identity is the completeness test
207
+ * used by `consistentRootsForSizes`.
208
+ */
209
+ export function mmrSizeForLeafCount(leaves) {
210
+ return 2n * leaves - BigInt(popcount(leaves));
211
+ }
196
212
  /** Population count of a non-negative BigInt. */
197
- function popcount(x) {
213
+ export function popcount(x) {
198
214
  let count = 0;
199
215
  let v = x;
200
216
  while (v > 0n) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forestrie/merklelog",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "MMR (merkle mountain range) proofs and massif storage format for Forestrie transparency logs",
package/src/index.ts CHANGED
@@ -71,9 +71,22 @@ export {
71
71
  verifyInclusion,
72
72
  verifyConsistency,
73
73
  consistentRoots,
74
+ consistentRootsForSizes,
74
75
  indexConsistencyProof,
75
76
  type ConsistencyProof,
76
77
  } from "./mmr/algorithms.js";
78
+ // Typed proof-shape rejections raised by consistentRootsForSizes (names
79
+ // mirror the Solidity IUnivocityErrors members) — plan-2609-10 s4.3, FOR-568.
80
+ export {
81
+ ConsistencyShapeError,
82
+ IncompleteTreeSize,
83
+ ConsistencyPeakCountMismatch,
84
+ ConsistencyPathLengthMismatch,
85
+ ConsistencyPathMalformed,
86
+ ConsistencyRootMismatch,
87
+ SizeMustIncrease,
88
+ SizeNotUint64,
89
+ } from "./mmr/errors.js";
77
90
  export { createSyncHasher } from "./mmr/algorithms-sync.js";
78
91
  export type { Proof, Peak } from "./mmr/types.js";
79
92
  export type { Hasher } from "./mmr/types.js";
@@ -86,6 +99,7 @@ export {
86
99
  peakMMRIndexes,
87
100
  peaksBitmap,
88
101
  peakIndexForLeafProof,
102
+ mmrSizeForLeafCount,
89
103
  indexHeight,
90
104
  firstMMRSize,
91
105
  massifIndexFromMMRIndex,
@@ -12,11 +12,36 @@
12
12
  */
13
13
 
14
14
  import type { Proof, Hasher } from "./types.js";
15
- import { inclusionProof, peakMMRIndexes, type NodeGetter } from "./proof.js";
15
+ import {
16
+ inclusionProof,
17
+ peakMMRIndexes,
18
+ bitLength,
19
+ popcount,
20
+ peaksBitmap,
21
+ mmrSizeForLeafCount,
22
+ type NodeGetter,
23
+ } from "./proof.js";
24
+ import {
25
+ ConsistencyPathLengthMismatch,
26
+ ConsistencyPathMalformed,
27
+ ConsistencyPeakCountMismatch,
28
+ ConsistencyRootMismatch,
29
+ IncompleteTreeSize,
30
+ SizeMustIncrease,
31
+ SizeNotUint64,
32
+ } from "./errors.js";
16
33
  import { Uint64 } from "../uint64/index.js";
17
34
  import { heightIndex } from "./math.js";
18
35
  import { arraysEqual } from "../utils/arrays.js";
19
36
 
37
+ /**
38
+ * Upper bound of the uint64 domain the MMR profile's sizes live in: Solidity
39
+ * and Go take every size as `uint64`, and a CBOR unsigned integer caps here
40
+ * too. `bigint` carries no such bound, so {@link consistentRootsForSizes}
41
+ * states it.
42
+ */
43
+ const MAX_UINT64 = (1n << 64n) - 1n;
44
+
20
45
  /**
21
46
  * Encodes a Uint64 as 8 bytes big-endian.
22
47
  *
@@ -185,6 +210,12 @@ export interface ConsistencyProof {
185
210
  * duplicate roots collapse (many old peaks share one new peak). Requires
186
211
  * one path per MMR(A) peak (draft: `len(peaks(ifrom)) == len(accumulatorfrom)`).
187
212
  *
213
+ * This fold does not enforce the proof shape the two sizes imply: it reads
214
+ * whatever path lengths it is given and collapses whatever roots coincide.
215
+ * Use {@link consistentRootsForSizes} for verification — it checks each path
216
+ * against the length MMR(A) -> MMR(B) fixes and requires the paths under one
217
+ * target peak to agree.
218
+ *
188
219
  * @param ifrom - last node index of the complete MMR(A) (`mmrSizeA - 1`)
189
220
  * @param accumulatorFrom - MMR(A) peak values, descending height order
190
221
  * @param paths - inclusion path per peak, proven in MMR(B)
@@ -222,50 +253,227 @@ export async function consistentRoots(
222
253
  return roots;
223
254
  }
224
255
 
256
+ /**
257
+ * Produce the peaks of MMR(sizeTo) that `paths` prove from the peaks of
258
+ * MMR(sizeFrom), requiring the paths to have exactly the shape the two sizes
259
+ * imply (draft-bryce-cose-receipts-mmr-profile, "Verifying the Receipt of
260
+ * consistency", with the draft's SHOULD on path lengths enforced in the same
261
+ * pass). Line-for-line port of the reference `consistent_roots_for_sizes`
262
+ * (algorithms.py) and of Solidity `consistentRootsForSizes`
263
+ * (univocity `src/algorithms/consistentRoots.sol`).
264
+ *
265
+ * Sizes are node counts (the MMR ending at index `i` has `i + 1` nodes).
266
+ * `sizeFrom` MUST be the size of the state the verifier already trusts; taken
267
+ * from the proof instead, the check is void. Only `sizeTo` is required to be
268
+ * a complete MMR size: every trusted size was itself a checked target.
269
+ *
270
+ * For a complete MMR the set bits of `peaksBitmap(size)` are the peak heights,
271
+ * high to low, which is accumulator order. Let `split` be the highest bit on
272
+ * which the two bitmaps differ; as `sizeTo > sizeFrom` the target has it and
273
+ * the origin does not. An origin peak above `split` is also a peak of the
274
+ * target: its path is empty and it is returned unchanged. Every origin peak
275
+ * below `split` is committed by the target peak of height `split`: its path
276
+ * has length `split - h`, and every such path must prove the same root. The
277
+ * target's remaining peaks lie below every origin peak, so no path reaches
278
+ * them; the prover supplies them separately and their count is returned.
279
+ *
280
+ * Because the shape is fixed by the sizes alone, no peak index list and no
281
+ * per-hop bookkeeping beyond the hash itself is needed: the bitmaps are
282
+ * iterated directly.
283
+ *
284
+ * @param hasher - cryptographic hasher instance
285
+ * @param sizeFrom - node count of the trusted origin state (0 for an empty log)
286
+ * @param sizeTo - node count of the target state; must exceed `sizeFrom` and
287
+ * must be a complete MMR size
288
+ * @param accumulatorFrom - peaks of MMR(sizeFrom), descending height order
289
+ * @param paths - one path per origin peak, in the same order
290
+ * @returns `roots`, the peaks of MMR(sizeTo) proven from the origin peaks in
291
+ * descending height order (the unchanged peaks, then the one proven root if
292
+ * any), and `expectedRight`, the number of MMR(sizeTo) peaks the prover must
293
+ * supply as right peaks. `roots` followed by those right peaks is the
294
+ * accumulator of MMR(sizeTo).
295
+ * @throws {SizeNotUint64} if either size is outside `[0, 2^64 - 1]`
296
+ * @throws {SizeMustIncrease} if `sizeTo <= sizeFrom`
297
+ * @throws {IncompleteTreeSize} if `sizeTo` is not a complete MMR size
298
+ * @throws {ConsistencyPeakCountMismatch} if `accumulatorFrom` or `paths` does
299
+ * not have one entry per origin peak
300
+ * @throws {ConsistencyPathMalformed} if `paths` is not a dense array of
301
+ * `Uint8Array[]`
302
+ * @throws {ConsistencyPathLengthMismatch} if a path length differs from the
303
+ * length the two sizes imply
304
+ * @throws {ConsistencyRootMismatch} if two paths under one target peak produce
305
+ * different roots
306
+ */
307
+ export async function consistentRootsForSizes(
308
+ hasher: Hasher,
309
+ sizeFrom: bigint,
310
+ sizeTo: bigint,
311
+ accumulatorFrom: Uint8Array[],
312
+ paths: Uint8Array[][],
313
+ ): Promise<{ roots: Uint8Array[]; expectedRight: number }> {
314
+ // The sizes are uint64 in Solidity and Go; `bigint` carries no such bound,
315
+ // so it is stated here before any arithmetic reads them.
316
+ if (sizeFrom < 0n || sizeFrom > MAX_UINT64) {
317
+ throw new SizeNotUint64("sizeFrom", sizeFrom);
318
+ }
319
+ if (sizeTo < 0n || sizeTo > MAX_UINT64) {
320
+ throw new SizeNotUint64("sizeTo", sizeTo);
321
+ }
322
+ if (sizeTo <= sizeFrom) {
323
+ throw new SizeMustIncrease(sizeFrom, sizeTo);
324
+ }
325
+ const to = peaksBitmap(sizeTo);
326
+ // peaksBitmap rounds an incomplete size down to the largest MMR below it,
327
+ // so `to` describes MMR(sizeTo) only if sizeTo is complete. Without this a
328
+ // target such as 6 anchors an accumulator that is no MMR's, and a verifier
329
+ // later reads its entries at the wrong heights.
330
+ if (mmrSizeForLeafCount(to) !== sizeTo) {
331
+ throw new IncompleteTreeSize(sizeTo);
332
+ }
333
+ const from = peaksBitmap(sizeFrom);
334
+ const n = popcount(from);
335
+ if (accumulatorFrom.length !== n) {
336
+ throw new ConsistencyPeakCountMismatch(n, accumulatorFrom.length);
337
+ }
338
+ if (!Array.isArray(paths)) {
339
+ throw new ConsistencyPathMalformed(`not an array (got ${typeName(paths)})`);
340
+ }
341
+ if (paths.length !== n) {
342
+ throw new ConsistencyPeakCountMismatch(n, paths.length);
343
+ }
344
+ assertDensePaths(paths);
345
+ const nto = popcount(to);
346
+ if (n === 0) {
347
+ return { roots: [], expectedRight: nto };
348
+ }
349
+
350
+ const split = bitLength(from ^ to) - 1;
351
+ const roots: Uint8Array[] = [];
352
+ // Nodes preceding the current origin peak's subtree; a peak of height h
353
+ // sits at offset + 2^(h+1) - 2 and its subtree has 2^(h+1) - 1 nodes.
354
+ let offset = 0n;
355
+ let i = 0;
356
+
357
+ // Origin peaks above the split are also peaks of the target. The path is
358
+ // not read; requiring it to be empty rejects unused material (a shape
359
+ // check: the result does not depend on it).
360
+ for (let h = bitLength(from) - 1; h > split; h--) {
361
+ if (((from >> BigInt(h)) & 1n) === 0n) continue;
362
+ if (paths[i].length !== 0) {
363
+ throw new ConsistencyPathLengthMismatch(i, 0, paths[i].length);
364
+ }
365
+ roots.push(accumulatorFrom[i]);
366
+ offset += (1n << BigInt(h + 1)) - 1n;
367
+ i += 1;
368
+ }
369
+
370
+ // Origin peaks below the split are all committed by the target peak of
371
+ // height `split` (bit `split` itself is clear in `from`), so each path must
372
+ // have length split - h and every path must prove the same root. The first
373
+ // `above` peaks were returned unchanged, so i === above at the first peak
374
+ // below the split.
375
+ const above = roots.length;
376
+ let root: Uint8Array | undefined;
377
+ for (let h = split - 1; h >= 0; h--) {
378
+ if (((from >> BigInt(h)) & 1n) === 0n) continue;
379
+ const expected = split - h;
380
+ if (paths[i].length !== expected) {
381
+ throw new ConsistencyPathLengthMismatch(i, expected, paths[i].length);
382
+ }
383
+ const subtree = (1n << BigInt(h + 1)) - 1n;
384
+ const peakMMRIndex = offset + subtree - 1n;
385
+ const proven = await calculateRoot(
386
+ hasher,
387
+ accumulatorFrom[i],
388
+ { path: paths[i], mmrIndex: peakMMRIndex },
389
+ peakMMRIndex,
390
+ );
391
+ if (i === above) {
392
+ root = proven;
393
+ } else if (!arraysEqual(proven, root as Uint8Array)) {
394
+ throw new ConsistencyRootMismatch(i);
395
+ }
396
+ offset += subtree;
397
+ i += 1;
398
+ }
399
+ if (n > above) {
400
+ roots.push(root as Uint8Array);
401
+ }
402
+
403
+ return { roots, expectedRight: nto - roots.length };
404
+ }
405
+
225
406
  /**
226
407
  * Verify MMR(A) is a committed prefix of MMR(B)
227
408
  * (draft-bryce "Verifying the Receipt of consistency";
228
409
  * go-merklelog `VerifyConsistency`).
229
410
  *
411
+ * BOTH sizes are CALLER-SUPPLIED trusted state (ADR-0066 D5.4), never read
412
+ * off the proof: `sizeFrom` is the size of the state `peaksFrom` is the
413
+ * accumulator of, and `sizeTo` the size `peaksTo` is claimed to be the
414
+ * accumulator of. The proof contributes only `paths`. Taking either size
415
+ * from the proof makes the check void: the fold constrains a declared
416
+ * `sizeFrom` by the origin peak COUNT alone, which a whole family of sizes
417
+ * share, so one set of paths and peaks verified at many size pairs — with
418
+ * the draft's own MMR(11) -> MMR(39) material, 24 distinct pairs, of which
419
+ * one is the true relation. `indexConsistencyProof` still returns the
420
+ * {@link ConsistencyProof} wire type with its declared sizes; a caller that
421
+ * relays that type passes `proof.paths` here and supplies the sizes it
422
+ * trusts, comparing the declared sizes with them separately if it wants to
423
+ * report a disagreement.
424
+ *
425
+ * Routed through {@link consistentRootsForSizes}, so the proof must have the
426
+ * shape `sizeFrom -> sizeTo` implies: one path per MMR(A) peak, empty above
427
+ * the split and of length `split - h` below it, with every path below the
428
+ * split proving the same root. The proven roots must then be the leading
429
+ * entries of `peaksTo`, and `peaksTo` must hold exactly those plus the
430
+ * `expectedRight` right peaks no path reaches — so a truncated or padded
431
+ * target accumulator is rejected on its length alone.
432
+ *
230
433
  * Replaces the plan-0027 always-true stub (FOR-368 Phase 1,
231
434
  * plan-2607-29): the previous signature took two inclusion proofs and
232
435
  * returned true unconditionally; no caller existed.
233
436
  *
234
- * @param proof - consistency proof `mmrSizeA -> mmrSizeB`
437
+ * @param sizeFrom - TRUSTED node count of MMR(A)
438
+ * @param sizeTo - TRUSTED node count of MMR(B)
235
439
  * @param peaksFrom - TRUSTED MMR(A) accumulator (e.g. a signed checkpoint
236
440
  * payload), descending height order
441
+ * @param paths - one inclusion path per MMR(A) peak, in the same order
237
442
  * @param peaksTo - MMR(B) accumulator to prove against (e.g. an anchored
238
443
  * on-chain state), descending height order
239
- * @returns ok, with the MMR(B) accumulator on success
444
+ * @returns ok, with the MMR(B) accumulator on success. A value that does not
445
+ * match `peaksTo` returns `{ok: false, accumulator: []}`; a proof that does
446
+ * not have the shape the sizes imply throws one of the typed errors in
447
+ * `./errors.js` (all `ConsistencyShapeError` subclasses), which callers
448
+ * report as a malformed proof rather than a failed comparison.
240
449
  */
241
450
  export async function verifyConsistency(
242
451
  hasher: Hasher,
243
- proof: ConsistencyProof,
452
+ sizeFrom: bigint,
453
+ sizeTo: bigint,
244
454
  peaksFrom: Uint8Array[],
455
+ paths: Uint8Array[][],
245
456
  peaksTo: Uint8Array[],
246
457
  ): Promise<{ ok: boolean; accumulator: Uint8Array[] }> {
247
- const proven = await consistentRoots(
458
+ const { roots, expectedRight } = await consistentRootsForSizes(
248
459
  hasher,
249
- proof.mmrSizeA - 1n,
460
+ sizeFrom,
461
+ sizeTo,
250
462
  peaksFrom,
251
- proof.paths,
463
+ paths,
252
464
  );
253
- // Both lists are in descending height order, so every proven root must
254
- // appear in order within peaksTo (a linear scan; go-merklelog semantics:
255
- // a proven root matches the current peak or exactly the next one down).
256
- let ito = 0;
257
- for (const root of proven) {
258
- if (ito < peaksTo.length && arraysEqual(peaksTo[ito], root)) {
259
- continue;
260
- }
261
- ito += 1;
262
- if (ito >= peaksTo.length || !arraysEqual(peaksTo[ito], root)) {
465
+ // roots is the leading run of the MMR(B) accumulator and expectedRight
466
+ // counts the peaks below every MMR(A) peak, which no path reaches. Together
467
+ // they fix the length of MMR(B)'s accumulator, so the supplied peaksTo must
468
+ // have that length and must start with the proven roots.
469
+ if (peaksTo.length !== roots.length + expectedRight) {
470
+ return { ok: false, accumulator: [] };
471
+ }
472
+ for (let i = 0; i < roots.length; i++) {
473
+ if (!arraysEqual(peaksTo[i], roots[i])) {
263
474
  return { ok: false, accumulator: [] };
264
475
  }
265
476
  }
266
- // The full MMR(B) accumulator is the proven prefix plus any right-peaks;
267
- // returning peaksTo is safe because consistentRoots enforced one proof
268
- // per MMR(A) peak (see go-merklelog VerifyConsistency).
269
477
  return { ok: true, accumulator: peaksTo };
270
478
  }
271
479
 
@@ -286,3 +494,40 @@ export function indexConsistencyProof(
286
494
  }
287
495
  return { mmrSizeA: mmrIndexA + 1n, mmrSizeB: mmrIndexB + 1n, paths };
288
496
  }
497
+
498
+ /** Name the value found where a path or a path element was required. */
499
+ function typeName(value: unknown): string {
500
+ if (value === null) return "null";
501
+ if (Array.isArray(value)) return "an array";
502
+ return typeof value;
503
+ }
504
+
505
+ /**
506
+ * Require `paths` to be a DENSE array of `Uint8Array[]`: no holes, and every
507
+ * element an array of `Uint8Array`. A hole and an out-of-type entry both
508
+ * survive the peak COUNT check and would otherwise reach the hashing loop,
509
+ * where they surface as a bare `TypeError` outside the
510
+ * `ConsistencyShapeError` family (review finding C6).
511
+ */
512
+ function assertDensePaths(paths: Uint8Array[][]): void {
513
+ for (let i = 0; i < paths.length; i++) {
514
+ if (!(i in paths)) {
515
+ throw new ConsistencyPathMalformed("missing (the array is sparse)", i);
516
+ }
517
+ const path = paths[i] as unknown;
518
+ if (!Array.isArray(path)) {
519
+ throw new ConsistencyPathMalformed(
520
+ `not an array (got ${typeName(path)})`,
521
+ i,
522
+ );
523
+ }
524
+ for (let j = 0; j < path.length; j++) {
525
+ if (!(j in path) || !((path[j] as unknown) instanceof Uint8Array)) {
526
+ throw new ConsistencyPathMalformed(
527
+ `element ${j} is not a Uint8Array (got ${typeName(path[j])})`,
528
+ i,
529
+ );
530
+ }
531
+ }
532
+ }
533
+ }