@forestrie/merklelog 0.4.0 → 0.6.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.
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@ export { Massif } from "./massifs/massif.js";
11
11
  export type { MassifStart, LeafComponent, LeafEnumeratorSpec, LeafEntry, } from "./massifs/types.js";
12
12
  export { MassifStartFmt } from "./massifs/types.js";
13
13
  export { LogFormat, Urkle, Bloom, IndexV2, leafCountForMassifHeight, leafTableBytes, nodeCountMax, nodeStoreBytes, bloomMBits, bloomBitsetBytes, bloomRegionBytes, indexDataBytesV2, peakStackEnd, massifLogEntries, urkleLeafTableStartFieldIndex, urkleLeafTableStartByteOffset, createLeafEnumerator, leafComponentByteOffset, leafComponentSize, computeLastMMRIndex, isMassifFull, } from "./massifs/types.js";
14
- export { parseV2StorageObjectPath } from "./massifs/v2storagepaths.js";
14
+ export { formatMassifObjectIndex, MASSIF_OBJECT_INDEX_DIGITS, MAX_MASSIF_INDEX, parseV2StorageObjectPath, v2CheckpointObjectKey, v2MassifObjectKey, } from "./massifs/v2storagepaths.js";
15
15
  export { openMassifNodeStore } from "./massifs/nodestore.js";
16
16
  export type { MassifNodeStore } from "./massifs/nodestore.js";
17
17
  export { openMassifLeafIndex, MissingIndexError } from "./massifs/leafindex.js";
@@ -19,7 +19,7 @@ export type { MassifLeafIndex, LocatedLeaf } from "./massifs/leafindex.js";
19
19
  export { mmrIndex, massifFirstLeaf, leafMinusSpurSum } from "./mmr/index.js";
20
20
  export { heightIndex, height, mmrIndexFromLeafIndex, leafIndex, mmrPosition, mmrSizeFromHeightIndex, leafCount, leafCountFromHeightIndex, } from "./mmr/math.js";
21
21
  export { bagPeaks, calculateRoot, verifyInclusion, verifyConsistency, consistentRoots, consistentRootsForSizes, indexConsistencyProof, type ConsistencyProof, } from "./mmr/algorithms.js";
22
- export { ConsistencyShapeError, IncompleteTreeSize, ConsistencyPeakCountMismatch, ConsistencyPathLengthMismatch, ConsistencyRootMismatch, SizeMustIncrease, } from "./mmr/errors.js";
22
+ export { ConsistencyShapeError, IncompleteTreeSize, ConsistencyPeakCountMismatch, ConsistencyPathLengthMismatch, ConsistencyPathMalformed, ConsistencyRootMismatch, SizeMustIncrease, SizeNotUint64, } from "./mmr/errors.js";
23
23
  export { createSyncHasher } from "./mmr/algorithms-sync.js";
24
24
  export type { Proof, Peak } from "./mmr/types.js";
25
25
  export type { Hasher } from "./mmr/types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,YAAY,EACV,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,SAAS,GACV,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EACL,SAAS,EACT,KAAK,EACL,KAAK,EACL,OAAO,EACP,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,gBAAgB,EAChB,6BAA6B,EAC7B,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,wBAAwB,EAAE,MAAM,6BAA6B,CAAC;AAGvE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChF,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG3E,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EACL,WAAW,EACX,MAAM,EACN,qBAAqB,EACrB,SAAS,EACT,WAAW,EACX,sBAAsB,EACtB,SAAS,EACT,wBAAwB,GACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,QAAQ,EACR,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,qBAAqB,EACrB,KAAK,gBAAgB,GACtB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,4BAA4B,EAC5B,6BAA6B,EAC7B,uBAAuB,EACvB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,yBAAyB,EACzB,cAAc,EACd,WAAW,EACX,qBAAqB,EACrB,mBAAmB,EACnB,WAAW,EACX,YAAY,EACZ,uBAAuB,EACvB,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,YAAY,EACV,WAAW,EACX,aAAa,EACb,kBAAkB,EAClB,SAAS,GACV,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EACL,SAAS,EACT,KAAK,EACL,KAAK,EACL,OAAO,EACP,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,YAAY,EACZ,gBAAgB,EAChB,6BAA6B,EAC7B,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,uBAAuB,EACvB,0BAA0B,EAC1B,gBAAgB,EAChB,wBAAwB,EACxB,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAChF,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG3E,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EACL,WAAW,EACX,MAAM,EACN,qBAAqB,EACrB,SAAS,EACT,WAAW,EACX,sBAAsB,EACtB,SAAS,EACT,wBAAwB,GACzB,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,QAAQ,EACR,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,uBAAuB,EACvB,qBAAqB,EACrB,KAAK,gBAAgB,GACtB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,qBAAqB,EACrB,kBAAkB,EAClB,4BAA4B,EAC5B,6BAA6B,EAC7B,wBAAwB,EACxB,uBAAuB,EACvB,gBAAgB,EAChB,aAAa,GACd,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,YAAY,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAG7C,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,yBAAyB,EACzB,cAAc,EACd,WAAW,EACX,qBAAqB,EACrB,mBAAmB,EACnB,WAAW,EACX,YAAY,EACZ,uBAAuB,EACvB,YAAY,GACb,MAAM,gBAAgB,CAAC;AACxB,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export { Uint64 } from "./uint64/index.js";
12
12
  export { Massif } from "./massifs/massif.js";
13
13
  export { MassifStartFmt } from "./massifs/types.js";
14
14
  export { LogFormat, Urkle, Bloom, IndexV2, leafCountForMassifHeight, leafTableBytes, nodeCountMax, nodeStoreBytes, bloomMBits, bloomBitsetBytes, bloomRegionBytes, indexDataBytesV2, peakStackEnd, massifLogEntries, urkleLeafTableStartFieldIndex, urkleLeafTableStartByteOffset, createLeafEnumerator, leafComponentByteOffset, leafComponentSize, computeLastMMRIndex, isMassifFull, } from "./massifs/types.js";
15
- export { parseV2StorageObjectPath } from "./massifs/v2storagepaths.js";
15
+ export { formatMassifObjectIndex, MASSIF_OBJECT_INDEX_DIGITS, MAX_MASSIF_INDEX, parseV2StorageObjectPath, v2CheckpointObjectKey, v2MassifObjectKey, } from "./massifs/v2storagepaths.js";
16
16
  // Massif blob node access for MMR proof building (plan-2607-15 §4)
17
17
  export { openMassifNodeStore } from "./massifs/nodestore.js";
18
18
  // Content-hash leaf lookup over the massif index region (FOR-373)
@@ -23,7 +23,7 @@ export { heightIndex, height, mmrIndexFromLeafIndex, leafIndex, mmrPosition, mmr
23
23
  export { bagPeaks, calculateRoot, verifyInclusion, verifyConsistency, consistentRoots, consistentRootsForSizes, indexConsistencyProof, } from "./mmr/algorithms.js";
24
24
  // Typed proof-shape rejections raised by consistentRootsForSizes (names
25
25
  // mirror the Solidity IUnivocityErrors members) — plan-2609-10 s4.3, FOR-568.
26
- export { ConsistencyShapeError, IncompleteTreeSize, ConsistencyPeakCountMismatch, ConsistencyPathLengthMismatch, ConsistencyRootMismatch, SizeMustIncrease, } from "./mmr/errors.js";
26
+ export { ConsistencyShapeError, IncompleteTreeSize, ConsistencyPeakCountMismatch, ConsistencyPathLengthMismatch, ConsistencyPathMalformed, ConsistencyRootMismatch, SizeMustIncrease, SizeNotUint64, } from "./mmr/errors.js";
27
27
  export { createSyncHasher } from "./mmr/algorithms-sync.js";
28
28
  // MMR proof building (pure; store-agnostic) — plan-2607-15 §4 (go: mmr/proof.go, peaks.go)
29
29
  export { inclusionProof, inclusionProofPath, inclusionProofLocalExtend, peakMMRIndexes, peaksBitmap, peakIndexForLeafProof, mmrSizeForLeafCount, indexHeight, firstMMRSize, massifIndexFromMMRIndex, peakStackMap, } from "./mmr/proof.js";
@@ -1,17 +1,57 @@
1
1
  /**
2
- * Parse an R2 object path to extract log ID, massif height, and massif index.
2
+ * Merklelog v2 R2 object layout:
3
3
  *
4
4
  * - v2/merklelog/massifs/{massifHeight}/{logId}/{massifIndex}.log
5
5
  * - v2/merklelog/checkpoints/{massifHeight}/{logId}/{massifIndex}.sth
6
6
  *
7
- * @param path - The object path path from the R2 notification
7
+ * `{massifIndex}` is a zero-padded, 16-digit DECIMAL integer. The Go writer
8
+ * owns this convention: go-merklelog
9
+ * [massifs/storage/const.go](https://github.com/forestrie/go-merklelog/blob/main/massifs/storage/const.go)
10
+ * (`V1MMRBlobNameFmt = "%016d.log"`, `V1MMRSignedTreeHeadBlobNameFmt =
11
+ * "%016d.sth"`) and its reader
12
+ * [objectindexfrompath.go](https://github.com/forestrie/go-merklelog/blob/main/massifs/storage/objectindexfrompath.go)
13
+ * (`strconv.Atoi`). canopy-api builds its R2 keys through the functions
14
+ * here (FOR-621: an earlier reader parsed hex, which agrees with the writer
15
+ * only for indices 0-9). Code outside this package that spells a key by hand
16
+ * (delegation-coordinator `seal-hint.ts`, test fixtures, demos) uses only
17
+ * index 0, which reads the same in any radix; anything that needs another
18
+ * index must use these helpers.
19
+ */
20
+ /** Width of the massif index segment in an object key. */
21
+ export declare const MASSIF_OBJECT_INDEX_DIGITS = 16;
22
+ /**
23
+ * Largest massif index the Go side can represent. go-merklelog
24
+ * `FmtMassifPath(prefix, massifIndex uint32)` and arbor's publisher
25
+ * (`strconv.ParseUint(base, 10, 32)`) use `uint32`; a larger index would be
26
+ * silently truncated by `ObjectIndexFromPath`, so it is rejected here.
27
+ */
28
+ export declare const MAX_MASSIF_INDEX = 4294967295n;
29
+ /**
30
+ * Parse an R2 object path to extract log ID, massif height, and massif index.
31
+ *
32
+ * @param path - The object path from the R2 notification
8
33
  * @returns Parsed components including logId, massifHeight, and massifIndex
9
34
  * @throws Error if the path doesn't match expected format or if parsing fails
10
35
  */
11
36
  export declare function parseV2StorageObjectPath(path: string): {
12
37
  logId: string;
13
38
  massifHeight: number;
14
- massifIndex: number;
39
+ massifIndex: bigint;
15
40
  type: string;
16
41
  };
42
+ /**
43
+ * Build the massif `.log` object key for a log.
44
+ */
45
+ export declare function v2MassifObjectKey(massifHeight: number, logId: string, massifIndex: bigint | number): string;
46
+ /**
47
+ * Build the signed checkpoint `.sth` object key for a log.
48
+ */
49
+ export declare function v2CheckpointObjectKey(massifHeight: number, logId: string, massifIndex: bigint | number): string;
50
+ /**
51
+ * Format a massif index as the 16-digit decimal key segment (Go `%016d`).
52
+ *
53
+ * @throws Error if the index is negative, not an integer, not a safe integer
54
+ * (number input), or exceeds {@link MAX_MASSIF_INDEX}.
55
+ */
56
+ export declare function formatMassifObjectIndex(massifIndex: bigint | number): string;
17
57
  //# sourceMappingURL=v2storagepaths.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"v2storagepaths.d.ts","sourceRoot":"","sources":["../../src/massifs/v2storagepaths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM;;;;;EAkDpD"}
1
+ {"version":3,"file":"v2storagepaths.d.ts","sourceRoot":"","sources":["../../src/massifs/v2storagepaths.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,0DAA0D;AAC1D,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAE7C;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAiB5C;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM;;;;;EAyDpD;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAAG,MAAM,GAC3B,MAAM,CAGR;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,YAAY,EAAE,MAAM,EACpB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAAG,MAAM,GAC3B,MAAM,CAGR;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAW5E"}
@@ -1,42 +1,84 @@
1
1
  /**
2
- * Parse an R2 object path to extract log ID, massif height, and massif index.
2
+ * Merklelog v2 R2 object layout:
3
3
  *
4
4
  * - v2/merklelog/massifs/{massifHeight}/{logId}/{massifIndex}.log
5
5
  * - v2/merklelog/checkpoints/{massifHeight}/{logId}/{massifIndex}.sth
6
6
  *
7
- * @param path - The object path path from the R2 notification
7
+ * `{massifIndex}` is a zero-padded, 16-digit DECIMAL integer. The Go writer
8
+ * owns this convention: go-merklelog
9
+ * [massifs/storage/const.go](https://github.com/forestrie/go-merklelog/blob/main/massifs/storage/const.go)
10
+ * (`V1MMRBlobNameFmt = "%016d.log"`, `V1MMRSignedTreeHeadBlobNameFmt =
11
+ * "%016d.sth"`) and its reader
12
+ * [objectindexfrompath.go](https://github.com/forestrie/go-merklelog/blob/main/massifs/storage/objectindexfrompath.go)
13
+ * (`strconv.Atoi`). canopy-api builds its R2 keys through the functions
14
+ * here (FOR-621: an earlier reader parsed hex, which agrees with the writer
15
+ * only for indices 0-9). Code outside this package that spells a key by hand
16
+ * (delegation-coordinator `seal-hint.ts`, test fixtures, demos) uses only
17
+ * index 0, which reads the same in any radix; anything that needs another
18
+ * index must use these helpers.
19
+ */
20
+ /** Width of the massif index segment in an object key. */
21
+ export const MASSIF_OBJECT_INDEX_DIGITS = 16;
22
+ /**
23
+ * Largest massif index the Go side can represent. go-merklelog
24
+ * `FmtMassifPath(prefix, massifIndex uint32)` and arbor's publisher
25
+ * (`strconv.ParseUint(base, 10, 32)`) use `uint32`; a larger index would be
26
+ * silently truncated by `ObjectIndexFromPath`, so it is rejected here.
27
+ */
28
+ export const MAX_MASSIF_INDEX = 0xffffffffn;
29
+ /**
30
+ * Massif height bounds. Go writes height as `%d` of a `uint8` and arbor's
31
+ * publisher `ParseCheckpointKey` rejects 0, so valid heights are 1..255 with
32
+ * no leading zeros.
33
+ */
34
+ const MIN_MASSIF_HEIGHT = 1;
35
+ const MAX_MASSIF_HEIGHT = 255;
36
+ const MASSIF_OBJECT_INDEX_RE = /^[0-9]{16}$/;
37
+ const MASSIF_HEIGHT_RE = /^[1-9][0-9]{0,2}$/;
38
+ const OBJECT_EXTENSIONS = {
39
+ massifs: ".log",
40
+ checkpoints: ".sth",
41
+ };
42
+ /**
43
+ * Parse an R2 object path to extract log ID, massif height, and massif index.
44
+ *
45
+ * @param path - The object path from the R2 notification
8
46
  * @returns Parsed components including logId, massifHeight, and massifIndex
9
47
  * @throws Error if the path doesn't match expected format or if parsing fails
10
48
  */
11
49
  export function parseV2StorageObjectPath(path) {
12
50
  const parts = path.split("/");
13
- if (parts.length < 6 || parts[0] !== "v2" || parts[1] !== "merklelog")
51
+ if (parts.length !== 6 || parts[0] !== "v2" || parts[1] !== "merklelog")
14
52
  throw new Error(`Unrecognized path format: ${path}`);
15
- // Check for new v2 format: v2/merklelog/massifs/{massifHeight}/{logId}/{index}.log
16
- // or v2/merklelog/checkpoints/{massifHeight}/{logId}/{index}.sth
17
- const typePart = parts[2]; // "massifs" or "checkpoints"
53
+ const typePart = parts[2];
18
54
  const massifHeightStr = parts[3];
19
55
  const logId = parts[4];
20
56
  const filename = parts[5];
21
- // Validate extension
22
- const expectedExt = typePart === "massifs" ? ".log" : ".sth";
57
+ const expectedExt = Object.hasOwn(OBJECT_EXTENSIONS, typePart)
58
+ ? OBJECT_EXTENSIONS[typePart]
59
+ : undefined;
60
+ if (expectedExt === undefined) {
61
+ throw new Error(`Object type must be massifs or checkpoints, got: ${typePart}`);
62
+ }
63
+ if (logId.length === 0) {
64
+ throw new Error(`Log id segment is empty: ${path}`);
65
+ }
23
66
  if (!filename.endsWith(expectedExt)) {
24
67
  throw new Error(`Expected ${expectedExt} extension for ${typePart}, got: ${filename}`);
25
68
  }
26
- // Parse massif index (hex, 16 digits)
27
69
  const massifIndexStr = filename.slice(0, -expectedExt.length);
28
- if (massifIndexStr.length !== 16) {
29
- throw new Error(`Massif index must be 16 hex digits, got ${massifIndexStr.length}: ${massifIndexStr}`);
70
+ if (!MASSIF_OBJECT_INDEX_RE.test(massifIndexStr)) {
71
+ throw new Error(`Massif index must be 16 decimal digits, got: ${massifIndexStr}`);
30
72
  }
31
- const massifIndex = Number.parseInt(massifIndexStr, 16);
32
- if (!Number.isFinite(massifIndex)) {
33
- throw new Error(`Failed to parse massif index as hex number: ${massifIndexStr}`);
73
+ const massifIndex = BigInt(massifIndexStr);
74
+ if (massifIndex > MAX_MASSIF_INDEX) {
75
+ throw new Error(`Massif index must be 0..${MAX_MASSIF_INDEX}, got ${massifIndex}`);
34
76
  }
35
- // Parse massifHeight
36
- const massifHeight = Number.parseInt(massifHeightStr, 10);
37
- if (!Number.isFinite(massifHeight)) {
77
+ if (!MASSIF_HEIGHT_RE.test(massifHeightStr) ||
78
+ Number(massifHeightStr) > MAX_MASSIF_HEIGHT) {
38
79
  throw new Error(`Failed to parse massif height as number: ${massifHeightStr}`);
39
80
  }
81
+ const massifHeight = Number(massifHeightStr);
40
82
  return {
41
83
  logId,
42
84
  massifHeight,
@@ -44,3 +86,48 @@ export function parseV2StorageObjectPath(path) {
44
86
  type: typePart,
45
87
  };
46
88
  }
89
+ /**
90
+ * Build the massif `.log` object key for a log.
91
+ */
92
+ export function v2MassifObjectKey(massifHeight, logId, massifIndex) {
93
+ assertKeyHeightAndLogId(massifHeight, logId);
94
+ return `v2/merklelog/massifs/${massifHeight}/${logId}/${formatMassifObjectIndex(massifIndex)}.log`;
95
+ }
96
+ /**
97
+ * Build the signed checkpoint `.sth` object key for a log.
98
+ */
99
+ export function v2CheckpointObjectKey(massifHeight, logId, massifIndex) {
100
+ assertKeyHeightAndLogId(massifHeight, logId);
101
+ return `v2/merklelog/checkpoints/${massifHeight}/${logId}/${formatMassifObjectIndex(massifIndex)}.sth`;
102
+ }
103
+ /**
104
+ * Format a massif index as the 16-digit decimal key segment (Go `%016d`).
105
+ *
106
+ * @throws Error if the index is negative, not an integer, not a safe integer
107
+ * (number input), or exceeds {@link MAX_MASSIF_INDEX}.
108
+ */
109
+ export function formatMassifObjectIndex(massifIndex) {
110
+ if (typeof massifIndex === "number" && !Number.isSafeInteger(massifIndex)) {
111
+ throw new Error(`Massif index must be a safe integer, got ${massifIndex}`);
112
+ }
113
+ const index = BigInt(massifIndex);
114
+ if (index < 0n || index > MAX_MASSIF_INDEX) {
115
+ throw new Error(`Massif index must be 0..${MAX_MASSIF_INDEX}, got ${index}`);
116
+ }
117
+ return index.toString(10).padStart(MASSIF_OBJECT_INDEX_DIGITS, "0");
118
+ }
119
+ /**
120
+ * Reject a massif height or log id that would build a key Go cannot read:
121
+ * height outside 1..255 or non-integer, or a log id that is empty or would
122
+ * add path segments.
123
+ */
124
+ function assertKeyHeightAndLogId(massifHeight, logId) {
125
+ if (!Number.isInteger(massifHeight) ||
126
+ massifHeight < MIN_MASSIF_HEIGHT ||
127
+ massifHeight > MAX_MASSIF_HEIGHT) {
128
+ throw new Error(`Massif height must be an integer ${MIN_MASSIF_HEIGHT}..${MAX_MASSIF_HEIGHT}, got ${massifHeight}`);
129
+ }
130
+ if (logId.length === 0 || logId.includes("/")) {
131
+ throw new Error(`Log id must be a non-empty path segment, got: ${JSON.stringify(logId)}`);
132
+ }
133
+ }
@@ -129,10 +129,13 @@ export declare function consistentRoots(hasher: Hasher, ifrom: bigint, accumulat
129
129
  * any), and `expectedRight`, the number of MMR(sizeTo) peaks the prover must
130
130
  * supply as right peaks. `roots` followed by those right peaks is the
131
131
  * accumulator of MMR(sizeTo).
132
+ * @throws {SizeNotUint64} if either size is outside `[0, 2^64 - 1]`
132
133
  * @throws {SizeMustIncrease} if `sizeTo <= sizeFrom`
133
134
  * @throws {IncompleteTreeSize} if `sizeTo` is not a complete MMR size
134
135
  * @throws {ConsistencyPeakCountMismatch} if `accumulatorFrom` or `paths` does
135
136
  * not have one entry per origin peak
137
+ * @throws {ConsistencyPathMalformed} if `paths` is not a dense array of
138
+ * `Uint8Array[]`
136
139
  * @throws {ConsistencyPathLengthMismatch} if a path length differs from the
137
140
  * length the two sizes imply
138
141
  * @throws {ConsistencyRootMismatch} if two paths under one target peak produce
@@ -1 +1 @@
1
- {"version":3,"file":"algorithms.d.ts","sourceRoot":"","sources":["../../src/mmr/algorithms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAOL,KAAK,UAAU,EAChB,MAAM,YAAY,CAAC;AA4BpB;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,UAAU,EAAE,GAClB,OAAO,CAAC,UAAU,CAAC,CAwBrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,UAAU,EACpB,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,CAAC,CAkCrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,UAAU,EACpB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,UAAU,EAAE,EAC7B,KAAK,EAAE,UAAU,EAAE,EAAE,GACpB,OAAO,CAAC,UAAU,EAAE,CAAC,CA0BvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,UAAU,EAAE,EAC7B,KAAK,EAAE,UAAU,EAAE,EAAE,GACpB,OAAO,CAAC;IAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC,CA+EzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,UAAU,EAAE,EACvB,KAAK,EAAE,UAAU,EAAE,EAAE,EACrB,OAAO,EAAE,UAAU,EAAE,GACpB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC,CAqBrD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,gBAAgB,CAMlB"}
1
+ {"version":3,"file":"algorithms.d.ts","sourceRoot":"","sources":["../../src/mmr/algorithms.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,EAOL,KAAK,UAAU,EAChB,MAAM,YAAY,CAAC;AAsCpB;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,UAAU,EAAE,GAClB,OAAO,CAAC,UAAU,CAAC,CAwBrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,UAAU,EACpB,KAAK,EAAE,KAAK,EACZ,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,CAAC,CAkCrB;AAED;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,UAAU,EACpB,KAAK,EAAE,KAAK,EACZ,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,mEAAmE;IACnE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,eAAe,EAAE,UAAU,EAAE,EAC7B,KAAK,EAAE,UAAU,EAAE,EAAE,GACpB,OAAO,CAAC,UAAU,EAAE,CAAC,CA0BvB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,UAAU,EAAE,EAC7B,KAAK,EAAE,UAAU,EAAE,EAAE,GACpB,OAAO,CAAC;IAAE,KAAK,EAAE,UAAU,EAAE,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC,CA2FzD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,UAAU,EAAE,EACvB,KAAK,EAAE,UAAU,EAAE,EAAE,EACrB,OAAO,EAAE,UAAU,EAAE,GACpB,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,UAAU,EAAE,CAAA;CAAE,CAAC,CAqBrD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,GAChB,gBAAgB,CAMlB"}
@@ -11,10 +11,17 @@
11
11
  * https://raw.githubusercontent.com/robinbryce/draft-bryce-cose-receipts-mmr-profile/refs/heads/main/draft-bryce-cose-receipts-mmr-profile.md
12
12
  */
13
13
  import { inclusionProof, peakMMRIndexes, bitLength, popcount, peaksBitmap, mmrSizeForLeafCount, } from "./proof.js";
14
- import { ConsistencyPathLengthMismatch, ConsistencyPeakCountMismatch, ConsistencyRootMismatch, IncompleteTreeSize, SizeMustIncrease, } from "./errors.js";
14
+ import { ConsistencyPathLengthMismatch, ConsistencyPathMalformed, ConsistencyPeakCountMismatch, ConsistencyRootMismatch, IncompleteTreeSize, SizeMustIncrease, SizeNotUint64, } from "./errors.js";
15
15
  import { Uint64 } from "../uint64/index.js";
16
16
  import { heightIndex } from "./math.js";
17
17
  import { arraysEqual } from "../utils/arrays.js";
18
+ /**
19
+ * Upper bound of the uint64 domain the MMR profile's sizes live in: Solidity
20
+ * and Go take every size as `uint64`, and a CBOR unsigned integer caps here
21
+ * too. `bigint` carries no such bound, so {@link consistentRootsForSizes}
22
+ * states it.
23
+ */
24
+ const MAX_UINT64 = (1n << 64n) - 1n;
18
25
  /**
19
26
  * Encodes a Uint64 as 8 bytes big-endian.
20
27
  *
@@ -208,16 +215,27 @@ export async function consistentRoots(hasher, ifrom, accumulatorFrom, paths) {
208
215
  * any), and `expectedRight`, the number of MMR(sizeTo) peaks the prover must
209
216
  * supply as right peaks. `roots` followed by those right peaks is the
210
217
  * accumulator of MMR(sizeTo).
218
+ * @throws {SizeNotUint64} if either size is outside `[0, 2^64 - 1]`
211
219
  * @throws {SizeMustIncrease} if `sizeTo <= sizeFrom`
212
220
  * @throws {IncompleteTreeSize} if `sizeTo` is not a complete MMR size
213
221
  * @throws {ConsistencyPeakCountMismatch} if `accumulatorFrom` or `paths` does
214
222
  * not have one entry per origin peak
223
+ * @throws {ConsistencyPathMalformed} if `paths` is not a dense array of
224
+ * `Uint8Array[]`
215
225
  * @throws {ConsistencyPathLengthMismatch} if a path length differs from the
216
226
  * length the two sizes imply
217
227
  * @throws {ConsistencyRootMismatch} if two paths under one target peak produce
218
228
  * different roots
219
229
  */
220
230
  export async function consistentRootsForSizes(hasher, sizeFrom, sizeTo, accumulatorFrom, paths) {
231
+ // The sizes are uint64 in Solidity and Go; `bigint` carries no such bound,
232
+ // so it is stated here before any arithmetic reads them.
233
+ if (sizeFrom < 0n || sizeFrom > MAX_UINT64) {
234
+ throw new SizeNotUint64("sizeFrom", sizeFrom);
235
+ }
236
+ if (sizeTo < 0n || sizeTo > MAX_UINT64) {
237
+ throw new SizeNotUint64("sizeTo", sizeTo);
238
+ }
221
239
  if (sizeTo <= sizeFrom) {
222
240
  throw new SizeMustIncrease(sizeFrom, sizeTo);
223
241
  }
@@ -234,9 +252,13 @@ export async function consistentRootsForSizes(hasher, sizeFrom, sizeTo, accumula
234
252
  if (accumulatorFrom.length !== n) {
235
253
  throw new ConsistencyPeakCountMismatch(n, accumulatorFrom.length);
236
254
  }
255
+ if (!Array.isArray(paths)) {
256
+ throw new ConsistencyPathMalformed(`not an array (got ${typeName(paths)})`);
257
+ }
237
258
  if (paths.length !== n) {
238
259
  throw new ConsistencyPeakCountMismatch(n, paths.length);
239
260
  }
261
+ assertDensePaths(paths);
240
262
  const nto = popcount(to);
241
263
  if (n === 0) {
242
264
  return { roots: [], expectedRight: nto };
@@ -364,3 +386,34 @@ export function indexConsistencyProof(get, mmrIndexA, mmrIndexB) {
364
386
  }
365
387
  return { mmrSizeA: mmrIndexA + 1n, mmrSizeB: mmrIndexB + 1n, paths };
366
388
  }
389
+ /** Name the value found where a path or a path element was required. */
390
+ function typeName(value) {
391
+ if (value === null)
392
+ return "null";
393
+ if (Array.isArray(value))
394
+ return "an array";
395
+ return typeof value;
396
+ }
397
+ /**
398
+ * Require `paths` to be a DENSE array of `Uint8Array[]`: no holes, and every
399
+ * element an array of `Uint8Array`. A hole and an out-of-type entry both
400
+ * survive the peak COUNT check and would otherwise reach the hashing loop,
401
+ * where they surface as a bare `TypeError` outside the
402
+ * `ConsistencyShapeError` family (review finding C6).
403
+ */
404
+ function assertDensePaths(paths) {
405
+ for (let i = 0; i < paths.length; i++) {
406
+ if (!(i in paths)) {
407
+ throw new ConsistencyPathMalformed("missing (the array is sparse)", i);
408
+ }
409
+ const path = paths[i];
410
+ if (!Array.isArray(path)) {
411
+ throw new ConsistencyPathMalformed(`not an array (got ${typeName(path)})`, i);
412
+ }
413
+ for (let j = 0; j < path.length; j++) {
414
+ if (!(j in path) || !(path[j] instanceof Uint8Array)) {
415
+ throw new ConsistencyPathMalformed(`element ${j} is not a Uint8Array (got ${typeName(path[j])})`, i);
416
+ }
417
+ }
418
+ }
419
+ }
@@ -66,4 +66,35 @@ export declare class SizeMustIncrease extends ConsistencyShapeError {
66
66
  readonly sizeTo: bigint;
67
67
  constructor(sizeFrom: bigint, sizeTo: bigint);
68
68
  }
69
+ /**
70
+ * A size is outside the uint64 domain the MMR profile's sizes live in.
71
+ *
72
+ * Solidity and Go take these sizes as `uint64`, so the domain is part of
73
+ * their signatures; TypeScript's `bigint` has no bound of its own. Without
74
+ * this check a size at or above 2^64 folds here and nowhere else: the
75
+ * position prefix `hash_pospair64` writes is truncated to eight bytes, so
76
+ * the hashes computed above the bound are not the ones the other two
77
+ * implementations would compute, and a size that large is not one a CBOR
78
+ * unsigned integer can carry or the contract can store either.
79
+ */
80
+ export declare class SizeNotUint64 extends ConsistencyShapeError {
81
+ readonly which: "sizeFrom" | "sizeTo";
82
+ readonly size: bigint;
83
+ constructor(which: "sizeFrom" | "sizeTo", size: bigint);
84
+ }
85
+ /**
86
+ * `paths` is not a dense array of `Uint8Array[]`.
87
+ *
88
+ * A hole (a sparse array), a `null`/`undefined` entry, a path that is not an
89
+ * array, or a path element that is not a `Uint8Array` all survive the peak
90
+ * COUNT check and then surface as a bare `TypeError` out of the fold, which
91
+ * escapes the {@link ConsistencyShapeError} family callers are told to
92
+ * switch on. The shape of the proof material is a proof-shape condition like
93
+ * any other, so it is reported as one.
94
+ */
95
+ export declare class ConsistencyPathMalformed extends ConsistencyShapeError {
96
+ readonly detail: string;
97
+ readonly index?: number | undefined;
98
+ constructor(detail: string, index?: number | undefined);
99
+ }
69
100
  //# sourceMappingURL=errors.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/mmr/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,8DAA8D;AAC9D,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;GAOG;AACH,qBAAa,kBAAmB,SAAQ,qBAAqB;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAIlC;AAED;;;GAGG;AACH,qBAAa,4BAA6B,SAAQ,qBAAqB;IAEnE,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM;gBADd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM;CAK1B;AAED;;;GAGG;AACH,qBAAa,6BAA8B,SAAQ,qBAAqB;IAEpE,QAAQ,CAAC,KAAK,EAAE,MAAM;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAFd,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM;CAK1B;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,qBAAqB;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM;gBAAb,KAAK,EAAE,MAAM;CAInC;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,qBAAqB;IAEvD,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM;gBADd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM;CAK1B"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/mmr/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,8DAA8D;AAC9D,qBAAa,qBAAsB,SAAQ,KAAK;gBAClC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;GAOG;AACH,qBAAa,kBAAmB,SAAQ,qBAAqB;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAIlC;AAED;;;GAGG;AACH,qBAAa,4BAA6B,SAAQ,qBAAqB;IAEnE,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM;gBADd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM;CAK1B;AAED;;;GAGG;AACH,qBAAa,6BAA8B,SAAQ,qBAAqB;IAEpE,QAAQ,CAAC,KAAK,EAAE,MAAM;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAFd,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM;CAK1B;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,SAAQ,qBAAqB;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM;gBAAb,KAAK,EAAE,MAAM;CAInC;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,qBAAqB;IAEvD,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM;gBADd,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM;CAK1B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,aAAc,SAAQ,qBAAqB;IAEpD,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,QAAQ;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM;gBADZ,KAAK,EAAE,UAAU,GAAG,QAAQ,EAC5B,IAAI,EAAE,MAAM;CAKxB;AAED;;;;;;;;;GASG;AACH,qBAAa,wBAAyB,SAAQ,qBAAqB;IAE/D,QAAQ,CAAC,MAAM,EAAE,MAAM;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM;gBADd,MAAM,EAAE,MAAM,EACd,KAAK,CAAC,EAAE,MAAM,YAAA;CAO1B"}
@@ -93,3 +93,44 @@ export class SizeMustIncrease extends ConsistencyShapeError {
93
93
  this.name = "SizeMustIncrease";
94
94
  }
95
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forestrie/merklelog",
3
- "version": "0.4.0",
3
+ "version": "0.6.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
@@ -43,7 +43,14 @@ export {
43
43
  isMassifFull,
44
44
  } from "./massifs/types.js";
45
45
 
46
- export { parseV2StorageObjectPath } from "./massifs/v2storagepaths.js";
46
+ export {
47
+ formatMassifObjectIndex,
48
+ MASSIF_OBJECT_INDEX_DIGITS,
49
+ MAX_MASSIF_INDEX,
50
+ parseV2StorageObjectPath,
51
+ v2CheckpointObjectKey,
52
+ v2MassifObjectKey,
53
+ } from "./massifs/v2storagepaths.js";
47
54
 
48
55
  // Massif blob node access for MMR proof building (plan-2607-15 §4)
49
56
  export { openMassifNodeStore } from "./massifs/nodestore.js";
@@ -82,8 +89,10 @@ export {
82
89
  IncompleteTreeSize,
83
90
  ConsistencyPeakCountMismatch,
84
91
  ConsistencyPathLengthMismatch,
92
+ ConsistencyPathMalformed,
85
93
  ConsistencyRootMismatch,
86
94
  SizeMustIncrease,
95
+ SizeNotUint64,
87
96
  } from "./mmr/errors.js";
88
97
  export { createSyncHasher } from "./mmr/algorithms-sync.js";
89
98
  export type { Proof, Peak } from "./mmr/types.js";
@@ -1,56 +1,106 @@
1
1
  /**
2
- * Parse an R2 object path to extract log ID, massif height, and massif index.
2
+ * Merklelog v2 R2 object layout:
3
3
  *
4
4
  * - v2/merklelog/massifs/{massifHeight}/{logId}/{massifIndex}.log
5
5
  * - v2/merklelog/checkpoints/{massifHeight}/{logId}/{massifIndex}.sth
6
6
  *
7
- * @param path - The object path path from the R2 notification
7
+ * `{massifIndex}` is a zero-padded, 16-digit DECIMAL integer. The Go writer
8
+ * owns this convention: go-merklelog
9
+ * [massifs/storage/const.go](https://github.com/forestrie/go-merklelog/blob/main/massifs/storage/const.go)
10
+ * (`V1MMRBlobNameFmt = "%016d.log"`, `V1MMRSignedTreeHeadBlobNameFmt =
11
+ * "%016d.sth"`) and its reader
12
+ * [objectindexfrompath.go](https://github.com/forestrie/go-merklelog/blob/main/massifs/storage/objectindexfrompath.go)
13
+ * (`strconv.Atoi`). canopy-api builds its R2 keys through the functions
14
+ * here (FOR-621: an earlier reader parsed hex, which agrees with the writer
15
+ * only for indices 0-9). Code outside this package that spells a key by hand
16
+ * (delegation-coordinator `seal-hint.ts`, test fixtures, demos) uses only
17
+ * index 0, which reads the same in any radix; anything that needs another
18
+ * index must use these helpers.
19
+ */
20
+
21
+ /** Width of the massif index segment in an object key. */
22
+ export const MASSIF_OBJECT_INDEX_DIGITS = 16;
23
+
24
+ /**
25
+ * Largest massif index the Go side can represent. go-merklelog
26
+ * `FmtMassifPath(prefix, massifIndex uint32)` and arbor's publisher
27
+ * (`strconv.ParseUint(base, 10, 32)`) use `uint32`; a larger index would be
28
+ * silently truncated by `ObjectIndexFromPath`, so it is rejected here.
29
+ */
30
+ export const MAX_MASSIF_INDEX = 0xffffffffn;
31
+
32
+ /**
33
+ * Massif height bounds. Go writes height as `%d` of a `uint8` and arbor's
34
+ * publisher `ParseCheckpointKey` rejects 0, so valid heights are 1..255 with
35
+ * no leading zeros.
36
+ */
37
+ const MIN_MASSIF_HEIGHT = 1;
38
+ const MAX_MASSIF_HEIGHT = 255;
39
+
40
+ const MASSIF_OBJECT_INDEX_RE = /^[0-9]{16}$/;
41
+ const MASSIF_HEIGHT_RE = /^[1-9][0-9]{0,2}$/;
42
+ const OBJECT_EXTENSIONS: Readonly<Record<string, string>> = {
43
+ massifs: ".log",
44
+ checkpoints: ".sth",
45
+ };
46
+
47
+ /**
48
+ * Parse an R2 object path to extract log ID, massif height, and massif index.
49
+ *
50
+ * @param path - The object path from the R2 notification
8
51
  * @returns Parsed components including logId, massifHeight, and massifIndex
9
52
  * @throws Error if the path doesn't match expected format or if parsing fails
10
53
  */
11
54
  export function parseV2StorageObjectPath(path: string) {
12
55
  const parts = path.split("/");
13
56
 
14
- if (parts.length < 6 || parts[0] !== "v2" || parts[1] !== "merklelog")
57
+ if (parts.length !== 6 || parts[0] !== "v2" || parts[1] !== "merklelog")
15
58
  throw new Error(`Unrecognized path format: ${path}`);
16
59
 
17
- // Check for new v2 format: v2/merklelog/massifs/{massifHeight}/{logId}/{index}.log
18
- // or v2/merklelog/checkpoints/{massifHeight}/{logId}/{index}.sth
19
- const typePart = parts[2]; // "massifs" or "checkpoints"
60
+ const typePart = parts[2];
20
61
  const massifHeightStr = parts[3];
21
62
  const logId = parts[4];
22
63
  const filename = parts[5];
23
64
 
24
- // Validate extension
25
- const expectedExt = typePart === "massifs" ? ".log" : ".sth";
65
+ const expectedExt = Object.hasOwn(OBJECT_EXTENSIONS, typePart)
66
+ ? OBJECT_EXTENSIONS[typePart]
67
+ : undefined;
68
+ if (expectedExt === undefined) {
69
+ throw new Error(
70
+ `Object type must be massifs or checkpoints, got: ${typePart}`,
71
+ );
72
+ }
73
+ if (logId.length === 0) {
74
+ throw new Error(`Log id segment is empty: ${path}`);
75
+ }
26
76
  if (!filename.endsWith(expectedExt)) {
27
77
  throw new Error(
28
78
  `Expected ${expectedExt} extension for ${typePart}, got: ${filename}`,
29
79
  );
30
80
  }
31
81
 
32
- // Parse massif index (hex, 16 digits)
33
82
  const massifIndexStr = filename.slice(0, -expectedExt.length);
34
- if (massifIndexStr.length !== 16) {
83
+ if (!MASSIF_OBJECT_INDEX_RE.test(massifIndexStr)) {
35
84
  throw new Error(
36
- `Massif index must be 16 hex digits, got ${massifIndexStr.length}: ${massifIndexStr}`,
85
+ `Massif index must be 16 decimal digits, got: ${massifIndexStr}`,
37
86
  );
38
87
  }
39
-
40
- const massifIndex = Number.parseInt(massifIndexStr, 16);
41
- if (!Number.isFinite(massifIndex)) {
88
+ const massifIndex = BigInt(massifIndexStr);
89
+ if (massifIndex > MAX_MASSIF_INDEX) {
42
90
  throw new Error(
43
- `Failed to parse massif index as hex number: ${massifIndexStr}`,
91
+ `Massif index must be 0..${MAX_MASSIF_INDEX}, got ${massifIndex}`,
44
92
  );
45
93
  }
46
94
 
47
- // Parse massifHeight
48
- const massifHeight = Number.parseInt(massifHeightStr, 10);
49
- if (!Number.isFinite(massifHeight)) {
95
+ if (
96
+ !MASSIF_HEIGHT_RE.test(massifHeightStr) ||
97
+ Number(massifHeightStr) > MAX_MASSIF_HEIGHT
98
+ ) {
50
99
  throw new Error(
51
100
  `Failed to parse massif height as number: ${massifHeightStr}`,
52
101
  );
53
102
  }
103
+ const massifHeight = Number(massifHeightStr);
54
104
 
55
105
  return {
56
106
  logId,
@@ -59,3 +109,68 @@ export function parseV2StorageObjectPath(path: string) {
59
109
  type: typePart,
60
110
  };
61
111
  }
112
+
113
+ /**
114
+ * Build the massif `.log` object key for a log.
115
+ */
116
+ export function v2MassifObjectKey(
117
+ massifHeight: number,
118
+ logId: string,
119
+ massifIndex: bigint | number,
120
+ ): string {
121
+ assertKeyHeightAndLogId(massifHeight, logId);
122
+ return `v2/merklelog/massifs/${massifHeight}/${logId}/${formatMassifObjectIndex(massifIndex)}.log`;
123
+ }
124
+
125
+ /**
126
+ * Build the signed checkpoint `.sth` object key for a log.
127
+ */
128
+ export function v2CheckpointObjectKey(
129
+ massifHeight: number,
130
+ logId: string,
131
+ massifIndex: bigint | number,
132
+ ): string {
133
+ assertKeyHeightAndLogId(massifHeight, logId);
134
+ return `v2/merklelog/checkpoints/${massifHeight}/${logId}/${formatMassifObjectIndex(massifIndex)}.sth`;
135
+ }
136
+
137
+ /**
138
+ * Format a massif index as the 16-digit decimal key segment (Go `%016d`).
139
+ *
140
+ * @throws Error if the index is negative, not an integer, not a safe integer
141
+ * (number input), or exceeds {@link MAX_MASSIF_INDEX}.
142
+ */
143
+ export function formatMassifObjectIndex(massifIndex: bigint | number): string {
144
+ if (typeof massifIndex === "number" && !Number.isSafeInteger(massifIndex)) {
145
+ throw new Error(`Massif index must be a safe integer, got ${massifIndex}`);
146
+ }
147
+ const index = BigInt(massifIndex);
148
+ if (index < 0n || index > MAX_MASSIF_INDEX) {
149
+ throw new Error(
150
+ `Massif index must be 0..${MAX_MASSIF_INDEX}, got ${index}`,
151
+ );
152
+ }
153
+ return index.toString(10).padStart(MASSIF_OBJECT_INDEX_DIGITS, "0");
154
+ }
155
+
156
+ /**
157
+ * Reject a massif height or log id that would build a key Go cannot read:
158
+ * height outside 1..255 or non-integer, or a log id that is empty or would
159
+ * add path segments.
160
+ */
161
+ function assertKeyHeightAndLogId(massifHeight: number, logId: string): void {
162
+ if (
163
+ !Number.isInteger(massifHeight) ||
164
+ massifHeight < MIN_MASSIF_HEIGHT ||
165
+ massifHeight > MAX_MASSIF_HEIGHT
166
+ ) {
167
+ throw new Error(
168
+ `Massif height must be an integer ${MIN_MASSIF_HEIGHT}..${MAX_MASSIF_HEIGHT}, got ${massifHeight}`,
169
+ );
170
+ }
171
+ if (logId.length === 0 || logId.includes("/")) {
172
+ throw new Error(
173
+ `Log id must be a non-empty path segment, got: ${JSON.stringify(logId)}`,
174
+ );
175
+ }
176
+ }
@@ -23,15 +23,25 @@ import {
23
23
  } from "./proof.js";
24
24
  import {
25
25
  ConsistencyPathLengthMismatch,
26
+ ConsistencyPathMalformed,
26
27
  ConsistencyPeakCountMismatch,
27
28
  ConsistencyRootMismatch,
28
29
  IncompleteTreeSize,
29
30
  SizeMustIncrease,
31
+ SizeNotUint64,
30
32
  } from "./errors.js";
31
33
  import { Uint64 } from "../uint64/index.js";
32
34
  import { heightIndex } from "./math.js";
33
35
  import { arraysEqual } from "../utils/arrays.js";
34
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
+
35
45
  /**
36
46
  * Encodes a Uint64 as 8 bytes big-endian.
37
47
  *
@@ -282,10 +292,13 @@ export async function consistentRoots(
282
292
  * any), and `expectedRight`, the number of MMR(sizeTo) peaks the prover must
283
293
  * supply as right peaks. `roots` followed by those right peaks is the
284
294
  * accumulator of MMR(sizeTo).
295
+ * @throws {SizeNotUint64} if either size is outside `[0, 2^64 - 1]`
285
296
  * @throws {SizeMustIncrease} if `sizeTo <= sizeFrom`
286
297
  * @throws {IncompleteTreeSize} if `sizeTo` is not a complete MMR size
287
298
  * @throws {ConsistencyPeakCountMismatch} if `accumulatorFrom` or `paths` does
288
299
  * not have one entry per origin peak
300
+ * @throws {ConsistencyPathMalformed} if `paths` is not a dense array of
301
+ * `Uint8Array[]`
289
302
  * @throws {ConsistencyPathLengthMismatch} if a path length differs from the
290
303
  * length the two sizes imply
291
304
  * @throws {ConsistencyRootMismatch} if two paths under one target peak produce
@@ -298,6 +311,14 @@ export async function consistentRootsForSizes(
298
311
  accumulatorFrom: Uint8Array[],
299
312
  paths: Uint8Array[][],
300
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
+ }
301
322
  if (sizeTo <= sizeFrom) {
302
323
  throw new SizeMustIncrease(sizeFrom, sizeTo);
303
324
  }
@@ -314,9 +335,13 @@ export async function consistentRootsForSizes(
314
335
  if (accumulatorFrom.length !== n) {
315
336
  throw new ConsistencyPeakCountMismatch(n, accumulatorFrom.length);
316
337
  }
338
+ if (!Array.isArray(paths)) {
339
+ throw new ConsistencyPathMalformed(`not an array (got ${typeName(paths)})`);
340
+ }
317
341
  if (paths.length !== n) {
318
342
  throw new ConsistencyPeakCountMismatch(n, paths.length);
319
343
  }
344
+ assertDensePaths(paths);
320
345
  const nto = popcount(to);
321
346
  if (n === 0) {
322
347
  return { roots: [], expectedRight: nto };
@@ -469,3 +494,40 @@ export function indexConsistencyProof(
469
494
  }
470
495
  return { mmrSizeA: mmrIndexA + 1n, mmrSizeB: mmrIndexB + 1n, paths };
471
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
+ }
package/src/mmr/errors.ts CHANGED
@@ -91,3 +91,46 @@ export class SizeMustIncrease extends ConsistencyShapeError {
91
91
  this.name = "SizeMustIncrease";
92
92
  }
93
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
+ }