@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
+
17
+ /** Base class for every consistency proof-shape rejection. */
18
+ export class ConsistencyShapeError extends Error {
19
+ constructor(message: string) {
20
+ super(message);
21
+ this.name = "ConsistencyShapeError";
22
+ }
23
+ }
24
+
25
+ /**
26
+ * The target size is not a complete MMR size.
27
+ *
28
+ * `peaksBitmap` rounds an incomplete size down to the largest complete MMR
29
+ * below it, so for an incomplete `sizeTo` the bitmap describes a different
30
+ * tree than the one named. Accepting it would anchor an accumulator that is
31
+ * no MMR's, and a later verifier would read its entries at the wrong heights.
32
+ */
33
+ export class IncompleteTreeSize extends ConsistencyShapeError {
34
+ constructor(readonly size: bigint) {
35
+ super(`size ${size} is not a complete MMR size`);
36
+ this.name = "IncompleteTreeSize";
37
+ }
38
+ }
39
+
40
+ /**
41
+ * The accumulator or the proof list does not have one entry per origin peak.
42
+ * The origin peak count is fixed by `sizeFrom` alone.
43
+ */
44
+ export class ConsistencyPeakCountMismatch extends ConsistencyShapeError {
45
+ constructor(
46
+ readonly expected: number,
47
+ readonly actual: number,
48
+ ) {
49
+ super(`expected ${expected} entries for the origin peaks, got ${actual}`);
50
+ this.name = "ConsistencyPeakCountMismatch";
51
+ }
52
+ }
53
+
54
+ /**
55
+ * A path does not have the length the two sizes imply: 0 for an origin peak
56
+ * above the split, `split - h` for an origin peak of height `h` below it.
57
+ */
58
+ export class ConsistencyPathLengthMismatch extends ConsistencyShapeError {
59
+ constructor(
60
+ readonly index: number,
61
+ readonly expected: number,
62
+ readonly actual: number,
63
+ ) {
64
+ super(`path ${index}: expected length ${expected}, got ${actual}`);
65
+ this.name = "ConsistencyPathLengthMismatch";
66
+ }
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
+ constructor(readonly index: number) {
76
+ super(`path ${index} proves a different root from the paths before it`);
77
+ this.name = "ConsistencyRootMismatch";
78
+ }
79
+ }
80
+
81
+ /**
82
+ * The target size does not exceed the origin size. Consistency is defined for
83
+ * a growing log; equal or decreasing sizes have no proof shape to check.
84
+ */
85
+ export class SizeMustIncrease extends ConsistencyShapeError {
86
+ constructor(
87
+ readonly sizeFrom: bigint,
88
+ readonly sizeTo: bigint,
89
+ ) {
90
+ super(`sizeTo (${sizeTo}) must exceed sizeFrom (${sizeFrom})`);
91
+ this.name = "SizeMustIncrease";
92
+ }
93
+ }
94
+
95
+ /**
96
+ * A size is outside the uint64 domain the MMR profile's sizes live in.
97
+ *
98
+ * Solidity and Go take these sizes as `uint64`, so the domain is part of
99
+ * their signatures; TypeScript's `bigint` has no bound of its own. Without
100
+ * this check a size at or above 2^64 folds here and nowhere else: the
101
+ * position prefix `hash_pospair64` writes is truncated to eight bytes, so
102
+ * the hashes computed above the bound are not the ones the other two
103
+ * implementations would compute, and a size that large is not one a CBOR
104
+ * unsigned integer can carry or the contract can store either.
105
+ */
106
+ export class SizeNotUint64 extends ConsistencyShapeError {
107
+ constructor(
108
+ readonly which: "sizeFrom" | "sizeTo",
109
+ readonly size: bigint,
110
+ ) {
111
+ super(`${which} (${size}) is outside the uint64 range [0, 2^64 - 1]`);
112
+ this.name = "SizeNotUint64";
113
+ }
114
+ }
115
+
116
+ /**
117
+ * `paths` is not a dense array of `Uint8Array[]`.
118
+ *
119
+ * A hole (a sparse array), a `null`/`undefined` entry, a path that is not an
120
+ * array, or a path element that is not a `Uint8Array` all survive the peak
121
+ * COUNT check and then surface as a bare `TypeError` out of the fold, which
122
+ * escapes the {@link ConsistencyShapeError} family callers are told to
123
+ * switch on. The shape of the proof material is a proof-shape condition like
124
+ * any other, so it is reported as one.
125
+ */
126
+ export class ConsistencyPathMalformed extends ConsistencyShapeError {
127
+ constructor(
128
+ readonly detail: string,
129
+ readonly index?: number,
130
+ ) {
131
+ super(
132
+ index === undefined ? `paths: ${detail}` : `path ${index}: ${detail}`,
133
+ );
134
+ this.name = "ConsistencyPathMalformed";
135
+ }
136
+ }
package/src/mmr/proof.ts CHANGED
@@ -14,7 +14,7 @@
14
14
  export type NodeGetter = (i: bigint) => Uint8Array;
15
15
 
16
16
  /** Number of bits needed to represent `num` (0 for 0). */
17
- function bitLength(num: bigint): number {
17
+ export function bitLength(num: bigint): number {
18
18
  if (num <= 0n) return 0;
19
19
  return num.toString(2).length;
20
20
  }
@@ -230,8 +230,25 @@ export function peaksBitmap(mmrSize: bigint): bigint {
230
230
  return peakMap;
231
231
  }
232
232
 
233
+ /**
234
+ * Node count of the complete MMR with `leaves` leaves (reference
235
+ * `mmr_size_for_leaf_count`, Solidity `mmrSizeForLeafCount`).
236
+ *
237
+ * Every leaf adds itself plus one interior node per binary carry, and each
238
+ * peak is a carry that has not happened, so the count is
239
+ * `2 * leaves - popcount(leaves)`.
240
+ *
241
+ * Because {@link peaksBitmap} rounds an incomplete size down to the largest
242
+ * complete MMR below it, `mmrSizeForLeafCount(peaksBitmap(size)) === size`
243
+ * holds exactly for complete sizes; that identity is the completeness test
244
+ * used by `consistentRootsForSizes`.
245
+ */
246
+ export function mmrSizeForLeafCount(leaves: bigint): bigint {
247
+ return 2n * leaves - BigInt(popcount(leaves));
248
+ }
249
+
233
250
  /** Population count of a non-negative BigInt. */
234
- function popcount(x: bigint): number {
251
+ export function popcount(x: bigint): number {
235
252
  let count = 0;
236
253
  let v = x;
237
254
  while (v > 0n) {