@forestrie/merklelog 0.5.0 → 0.7.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,8 @@ 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
+ export { massifIndexFromLeafIndex } from "./massifs/massifindex.js";
15
16
  export { openMassifNodeStore } from "./massifs/nodestore.js";
16
17
  export type { MassifNodeStore } from "./massifs/nodestore.js";
17
18
  export { openMassifLeafIndex, MissingIndexError } from "./massifs/leafindex.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,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"}
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,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AACpE,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,8 +12,9 @@ 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
+ export { massifIndexFromLeafIndex } from "./massifs/massifindex.js";
17
18
  export { openMassifNodeStore } from "./massifs/nodestore.js";
18
19
  // Content-hash leaf lookup over the massif index region (FOR-373)
19
20
  export { openMassifLeafIndex, MissingIndexError } from "./massifs/leafindex.js";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Which massif holds a leaf. A massif of one-based height h holds
3
+ * 2^(h-1) leaves; see {@link leafCountForMassifHeight} and the
4
+ * go-merklelog notation in
5
+ * [mmr-math-cheatsheet.md](https://github.com/forestrie/go-merklelog/blob/main/mmr-math-cheatsheet.md).
6
+ * Mirrors go-merklelog `massifs/massifindex.go` MassifIndexFromLeafIndex,
7
+ * which arbor's ranger uses when it commits massifs.
8
+ */
9
+ /**
10
+ * Returns the zero-based index of the massif that holds the leaf at
11
+ * `leafIndex` (zero-based, counting leaves only).
12
+ *
13
+ * @param massifHeight - One-based massif height (h >= 1)
14
+ * @throws RangeError when massifHeight is 0, which holds no leaves
15
+ */
16
+ export declare function massifIndexFromLeafIndex(massifHeight: number, leafIndex: bigint | number): bigint;
17
+ //# sourceMappingURL=massifindex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"massifindex.d.ts","sourceRoot":"","sources":["../../src/massifs/massifindex.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,GAAG,MAAM,GACzB,MAAM,CAMR"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Which massif holds a leaf. A massif of one-based height h holds
3
+ * 2^(h-1) leaves; see {@link leafCountForMassifHeight} and the
4
+ * go-merklelog notation in
5
+ * [mmr-math-cheatsheet.md](https://github.com/forestrie/go-merklelog/blob/main/mmr-math-cheatsheet.md).
6
+ * Mirrors go-merklelog `massifs/massifindex.go` MassifIndexFromLeafIndex,
7
+ * which arbor's ranger uses when it commits massifs.
8
+ */
9
+ import { leafCountForMassifHeight } from "./indexformat.js";
10
+ /**
11
+ * Returns the zero-based index of the massif that holds the leaf at
12
+ * `leafIndex` (zero-based, counting leaves only).
13
+ *
14
+ * @param massifHeight - One-based massif height (h >= 1)
15
+ * @throws RangeError when massifHeight is 0, which holds no leaves
16
+ */
17
+ export function massifIndexFromLeafIndex(massifHeight, leafIndex) {
18
+ const leavesPerMassif = leafCountForMassifHeight(massifHeight);
19
+ if (leavesPerMassif === 0n) {
20
+ throw new RangeError("massifHeight must be at least 1");
21
+ }
22
+ return BigInt(leafIndex) / leavesPerMassif;
23
+ }
@@ -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
+ }
@@ -21,7 +21,7 @@ export declare function leafMinusSpurSum(leafIndex: bigint): bigint;
21
21
  *
22
22
  * Translated from go-merklelog/massifs/massifstart.go MassifFirstLeaf
23
23
  */
24
- export declare function massifFirstLeaf(massifHeight: number, massifIndex: number): bigint;
24
+ export declare function massifFirstLeaf(massifHeight: number, massifIndex: number | bigint): bigint;
25
25
  /**
26
26
  * Returns the node index for the leaf e
27
27
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mmr/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQ1D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,GAClB,MAAM,CAYR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAalD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/mmr/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAQ1D;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,GAAG,MAAM,GAC3B,MAAM,CAaR;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAalD"}
package/dist/mmr/index.js CHANGED
@@ -30,8 +30,9 @@ export function leafMinusSpurSum(leafIndex) {
30
30
  * Translated from go-merklelog/massifs/massifstart.go MassifFirstLeaf
31
31
  */
32
32
  export function massifFirstLeaf(massifHeight, massifIndex) {
33
- // The number of nodes 'm' in a massif is: m = (1 << h) - 1
34
- const m = BigInt((1 << massifHeight) - 1);
33
+ // The number of nodes 'm' in a massif is: m = (1 << h) - 1. BigInt shift:
34
+ // a JS number `1 << h` is negative at h=31 and wraps at h=32.
35
+ const m = (1n << BigInt(massifHeight)) - 1n;
35
36
  // The number of leaves 'f' in every massif: f = (m + 1) / 2
36
37
  const f = (m + 1n) >> 1n;
37
38
  // The first leaf index is then: leafIndex = f * massifIndex
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forestrie/merklelog",
3
- "version": "0.5.0",
3
+ "version": "0.7.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,9 +43,17 @@ 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)
56
+ export { massifIndexFromLeafIndex } from "./massifs/massifindex.js";
49
57
  export { openMassifNodeStore } from "./massifs/nodestore.js";
50
58
  export type { MassifNodeStore } from "./massifs/nodestore.js";
51
59
 
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Which massif holds a leaf. A massif of one-based height h holds
3
+ * 2^(h-1) leaves; see {@link leafCountForMassifHeight} and the
4
+ * go-merklelog notation in
5
+ * [mmr-math-cheatsheet.md](https://github.com/forestrie/go-merklelog/blob/main/mmr-math-cheatsheet.md).
6
+ * Mirrors go-merklelog `massifs/massifindex.go` MassifIndexFromLeafIndex,
7
+ * which arbor's ranger uses when it commits massifs.
8
+ */
9
+
10
+ import { leafCountForMassifHeight } from "./indexformat.js";
11
+
12
+ /**
13
+ * Returns the zero-based index of the massif that holds the leaf at
14
+ * `leafIndex` (zero-based, counting leaves only).
15
+ *
16
+ * @param massifHeight - One-based massif height (h >= 1)
17
+ * @throws RangeError when massifHeight is 0, which holds no leaves
18
+ */
19
+ export function massifIndexFromLeafIndex(
20
+ massifHeight: number,
21
+ leafIndex: bigint | number,
22
+ ): bigint {
23
+ const leavesPerMassif = leafCountForMassifHeight(massifHeight);
24
+ if (leavesPerMassif === 0n) {
25
+ throw new RangeError("massifHeight must be at least 1");
26
+ }
27
+ return BigInt(leafIndex) / leavesPerMassif;
28
+ }
@@ -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
+ }
package/src/mmr/index.ts CHANGED
@@ -33,10 +33,11 @@ export function leafMinusSpurSum(leafIndex: bigint): bigint {
33
33
  */
34
34
  export function massifFirstLeaf(
35
35
  massifHeight: number,
36
- massifIndex: number,
36
+ massifIndex: number | bigint,
37
37
  ): bigint {
38
- // The number of nodes 'm' in a massif is: m = (1 << h) - 1
39
- const m = BigInt((1 << massifHeight) - 1);
38
+ // The number of nodes 'm' in a massif is: m = (1 << h) - 1. BigInt shift:
39
+ // a JS number `1 << h` is negative at h=31 and wraps at h=32.
40
+ const m = (1n << BigInt(massifHeight)) - 1n;
40
41
 
41
42
  // The number of leaves 'f' in every massif: f = (m + 1) / 2
42
43
  const f = (m + 1n) >> 1n;