@forestrie/merklelog 0.5.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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/massifs/v2storagepaths.d.ts +43 -3
- package/dist/massifs/v2storagepaths.d.ts.map +1 -1
- package/dist/massifs/v2storagepaths.js +104 -17
- package/package.json +1 -1
- package/src/index.ts +8 -1
- package/src/massifs/v2storagepaths.ts +133 -18
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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)
|
|
@@ -1,17 +1,57 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
*
|
|
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:
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
51
|
+
if (parts.length !== 6 || parts[0] !== "v2" || parts[1] !== "merklelog")
|
|
14
52
|
throw new Error(`Unrecognized path format: ${path}`);
|
|
15
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
29
|
-
throw new Error(`Massif index must be 16
|
|
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 =
|
|
32
|
-
if (
|
|
33
|
-
throw new Error(`
|
|
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
|
-
|
|
36
|
-
|
|
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
|
+
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -43,7 +43,14 @@ export {
|
|
|
43
43
|
isMassifFull,
|
|
44
44
|
} from "./massifs/types.js";
|
|
45
45
|
|
|
46
|
-
export {
|
|
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";
|
|
@@ -1,56 +1,106 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
-
*
|
|
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
|
|
57
|
+
if (parts.length !== 6 || parts[0] !== "v2" || parts[1] !== "merklelog")
|
|
15
58
|
throw new Error(`Unrecognized path format: ${path}`);
|
|
16
59
|
|
|
17
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
|
83
|
+
if (!MASSIF_OBJECT_INDEX_RE.test(massifIndexStr)) {
|
|
35
84
|
throw new Error(
|
|
36
|
-
`Massif index must be 16
|
|
85
|
+
`Massif index must be 16 decimal digits, got: ${massifIndexStr}`,
|
|
37
86
|
);
|
|
38
87
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (!Number.isFinite(massifIndex)) {
|
|
88
|
+
const massifIndex = BigInt(massifIndexStr);
|
|
89
|
+
if (massifIndex > MAX_MASSIF_INDEX) {
|
|
42
90
|
throw new Error(
|
|
43
|
-
`
|
|
91
|
+
`Massif index must be 0..${MAX_MASSIF_INDEX}, got ${massifIndex}`,
|
|
44
92
|
);
|
|
45
93
|
}
|
|
46
94
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
}
|