@forestrie/merklelog 0.0.1

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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/index.d.ts +21 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +20 -0
  6. package/dist/massifs/indexformat.d.ts +146 -0
  7. package/dist/massifs/indexformat.d.ts.map +1 -0
  8. package/dist/massifs/indexformat.js +191 -0
  9. package/dist/massifs/logformat.d.ts +49 -0
  10. package/dist/massifs/logformat.d.ts.map +1 -0
  11. package/dist/massifs/logformat.js +49 -0
  12. package/dist/massifs/massif.d.ts +86 -0
  13. package/dist/massifs/massif.d.ts.map +1 -0
  14. package/dist/massifs/massif.js +143 -0
  15. package/dist/massifs/massiffull.d.ts +14 -0
  16. package/dist/massifs/massiffull.d.ts.map +1 -0
  17. package/dist/massifs/massiffull.js +21 -0
  18. package/dist/massifs/massiflogentries.d.ts +23 -0
  19. package/dist/massifs/massiflogentries.d.ts.map +1 -0
  20. package/dist/massifs/massiflogentries.js +31 -0
  21. package/dist/massifs/massifstart.d.ts +63 -0
  22. package/dist/massifs/massifstart.d.ts.map +1 -0
  23. package/dist/massifs/massifstart.js +39 -0
  24. package/dist/massifs/mmrindex.d.ts +15 -0
  25. package/dist/massifs/mmrindex.d.ts.map +1 -0
  26. package/dist/massifs/mmrindex.js +24 -0
  27. package/dist/massifs/peakstackend.d.ts +21 -0
  28. package/dist/massifs/peakstackend.d.ts.map +1 -0
  29. package/dist/massifs/peakstackend.js +29 -0
  30. package/dist/massifs/types.d.ts +20 -0
  31. package/dist/massifs/types.d.ts.map +1 -0
  32. package/dist/massifs/types.js +23 -0
  33. package/dist/massifs/urkleindex.d.ts +116 -0
  34. package/dist/massifs/urkleindex.d.ts.map +1 -0
  35. package/dist/massifs/urkleindex.js +169 -0
  36. package/dist/massifs/v2storagepaths.d.ts +17 -0
  37. package/dist/massifs/v2storagepaths.d.ts.map +1 -0
  38. package/dist/massifs/v2storagepaths.js +46 -0
  39. package/dist/mmr/algorithms-sync.d.ts +17 -0
  40. package/dist/mmr/algorithms-sync.d.ts.map +1 -0
  41. package/dist/mmr/algorithms-sync.js +42 -0
  42. package/dist/mmr/algorithms.d.ts +79 -0
  43. package/dist/mmr/algorithms.d.ts.map +1 -0
  44. package/dist/mmr/algorithms.js +155 -0
  45. package/dist/mmr/index.d.ts +37 -0
  46. package/dist/mmr/index.d.ts.map +1 -0
  47. package/dist/mmr/index.js +64 -0
  48. package/dist/mmr/math.d.ts +67 -0
  49. package/dist/mmr/math.d.ts.map +1 -0
  50. package/dist/mmr/math.js +172 -0
  51. package/dist/mmr/types.d.ts +37 -0
  52. package/dist/mmr/types.d.ts.map +1 -0
  53. package/dist/mmr/types.js +4 -0
  54. package/dist/uint64/index.d.ts +90 -0
  55. package/dist/uint64/index.d.ts.map +1 -0
  56. package/dist/uint64/index.js +149 -0
  57. package/dist/utils/arrays.d.ts +15 -0
  58. package/dist/utils/arrays.d.ts.map +1 -0
  59. package/dist/utils/arrays.js +24 -0
  60. package/package.json +41 -0
  61. package/src/index.ts +68 -0
  62. package/src/massifs/indexformat.ts +217 -0
  63. package/src/massifs/logformat.ts +54 -0
  64. package/src/massifs/massif.ts +182 -0
  65. package/src/massifs/massiffull.ts +28 -0
  66. package/src/massifs/massiflogentries.ts +40 -0
  67. package/src/massifs/massifstart.ts +67 -0
  68. package/src/massifs/mmrindex.ts +32 -0
  69. package/src/massifs/peakstackend.ts +34 -0
  70. package/src/massifs/types.ts +55 -0
  71. package/src/massifs/urkleindex.ts +273 -0
  72. package/src/massifs/v2storagepaths.ts +61 -0
  73. package/src/mmr/algorithms-sync.ts +46 -0
  74. package/src/mmr/algorithms.ts +194 -0
  75. package/src/mmr/index.ts +75 -0
  76. package/src/mmr/math.ts +190 -0
  77. package/src/mmr/types.ts +39 -0
  78. package/src/uint64/index.ts +164 -0
  79. package/src/utils/arrays.ts +25 -0
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Urkle index helpers for efficient leaf data access
3
+ *
4
+ * Provides helpers for computing field indices and efficiently enumerating
5
+ * urkle leaf data components.
6
+ */
7
+ /**
8
+ * Leaf components that can be read via the enumerator.
9
+ */
10
+ export type LeafComponent = "idtimestamp" | "valueBytes" | "extra1" | "extra2" | "extra3";
11
+ /**
12
+ * Returns the field index (for use with Massif.fieldref) of the start of the
13
+ * urkle leaf table data region.
14
+ *
15
+ * The field index is the offset in units of ValueBytes (32 bytes), which can be
16
+ * passed directly to Massif.fieldref() to get a view of the data.
17
+ *
18
+ * V2 layout before leaf table:
19
+ * StartHeader (256 bytes = 8 fields)
20
+ * IndexHeader (32 bytes = 1 field)
21
+ * BloomBitsets (4 * ceil(mBits/8))
22
+ * FrontierState (544 bytes)
23
+ *
24
+ * @param massifHeight - One-based massif height
25
+ * @returns Field index (in units of 32-byte fields) where the leaf table starts
26
+ */
27
+ export declare function urkleLeafTableStartFieldIndex(massifHeight: number): number;
28
+ /**
29
+ * Returns the byte offset within a massif buffer where the urkle leaf table
30
+ * starts. This is useful for direct byte-level access.
31
+ *
32
+ * @param massifHeight - One-based massif height
33
+ * @returns Byte offset where the leaf table starts
34
+ */
35
+ export declare function urkleLeafTableStartByteOffset(massifHeight: number): number;
36
+ /**
37
+ * LeafEnumeratorSpec specifies which leaf components to enumerate.
38
+ *
39
+ * Each component that is true will be read and returned during enumeration.
40
+ */
41
+ export interface LeafEnumeratorSpec {
42
+ idtimestamp?: boolean;
43
+ valueBytes?: boolean;
44
+ extra1?: boolean;
45
+ extra2?: boolean;
46
+ extra3?: boolean;
47
+ }
48
+ /**
49
+ * LeafEntry represents a single leaf's data components.
50
+ *
51
+ * Only the components specified in the enumerator spec will be populated.
52
+ * Each component provides a DataView for efficient typed access.
53
+ */
54
+ export interface LeafEntry {
55
+ /** Leaf ordinal (0-based index) */
56
+ ordinal: number;
57
+ /** ID timestamp as bigint (if idtimestamp was requested) */
58
+ idtimestamp?: bigint;
59
+ /** Committed value bytes as Uint8Array view (if valueBytes was requested) */
60
+ valueBytes?: Uint8Array;
61
+ /** Extra1 field as Uint8Array view (24 bytes, if extra1 was requested) */
62
+ extra1?: Uint8Array;
63
+ /** Extra2 field as Uint8Array view (32 bytes, if extra2 was requested) */
64
+ extra2?: Uint8Array;
65
+ /** Extra3 field as Uint8Array view (32 bytes, if extra3 was requested) */
66
+ extra3?: Uint8Array;
67
+ }
68
+ /**
69
+ * Creates an efficient leaf enumerator for iterating through urkle leaf data.
70
+ *
71
+ * The enumerator returns a generator function that yields LeafEntry objects
72
+ * for each leaf in the massif. Only the components specified in the spec
73
+ * are read and returned, minimizing overhead.
74
+ *
75
+ * The enumerator uses DataView for efficient typed access without copying.
76
+ *
77
+ * @param buffer - The massif buffer (Uint8Array)
78
+ * @param massifHeight - One-based massif height
79
+ * @param leafCount - Number of leaves to enumerate (from start position)
80
+ * @param spec - Specification of which components to read
81
+ * @param start - Starting leaf ordinal (0-based, defaults to 0)
82
+ * @returns Generator that yields LeafEntry objects
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * const enumerate = createLeafEnumerator(
87
+ * massif.buffer,
88
+ * massifHeight,
89
+ * actualLeafCount,
90
+ * { idtimestamp: true, valueBytes: true },
91
+ * 0 // start from first leaf
92
+ * );
93
+ *
94
+ * for (const leaf of enumerate()) {
95
+ * console.log(leaf.ordinal, leaf.idtimestamp, leaf.valueBytes);
96
+ * }
97
+ * ```
98
+ */
99
+ export declare function createLeafEnumerator(buffer: Uint8Array, massifHeight: number, leafCount: number, spec: LeafEnumeratorSpec, start?: number): () => Generator<LeafEntry, void, unknown>;
100
+ /**
101
+ * Returns the byte offset of a specific leaf record's component.
102
+ *
103
+ * @param massifHeight - One-based massif height
104
+ * @param leafOrdinal - Zero-based leaf ordinal
105
+ * @param component - Which component to locate
106
+ * @returns Byte offset of the component within the massif buffer
107
+ */
108
+ export declare function leafComponentByteOffset(massifHeight: number, leafOrdinal: number, component: LeafComponent): number;
109
+ /**
110
+ * Returns the size in bytes of a leaf component.
111
+ *
112
+ * @param component - Which component
113
+ * @returns Size in bytes
114
+ */
115
+ export declare function leafComponentSize(component: LeafComponent): number;
116
+ //# sourceMappingURL=urkleindex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"urkleindex.d.ts","sourceRoot":"","sources":["../../src/massifs/urkleindex.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAWH;;GAEG;AACH,MAAM,MAAM,aAAa,GACrB,aAAa,GACb,YAAY,GACZ,QAAQ,GACR,QAAQ,GACR,QAAQ,CAAC;AAwBb;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAiB1E;AAED;;;;;;GAMG;AACH,wBAAgB,6BAA6B,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAa1E;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,UAAU,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,UAAU,EAClB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,kBAAkB,EACxB,KAAK,GAAE,MAAU,GAChB,MAAM,SAAS,CAAC,SAAS,EAAE,IAAI,EAAE,OAAO,CAAC,CAoE3C;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,EACpB,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,aAAa,GACvB,MAAM,CAIR;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,aAAa,GAAG,MAAM,CAElE"}
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Urkle index helpers for efficient leaf data access
3
+ *
4
+ * Provides helpers for computing field indices and efficiently enumerating
5
+ * urkle leaf data components.
6
+ */
7
+ import { LogFormat } from "./logformat.js";
8
+ import { Urkle, Bloom, leafCountForMassifHeight, bloomMBits, bloomBitsetBytes, } from "./indexformat.js";
9
+ /**
10
+ * Byte offsets within a leaf record for each component.
11
+ */
12
+ const LEAF_COMPONENT_OFFSETS = {
13
+ idtimestamp: 0,
14
+ valueBytes: Urkle.LeafValueOffset, // 8
15
+ extra1: Urkle.LeafExtra1Offset, // 40
16
+ extra2: Urkle.LeafExtra2Offset, // 64
17
+ extra3: Urkle.LeafExtra3Offset, // 96
18
+ };
19
+ /**
20
+ * Byte sizes for each leaf component.
21
+ */
22
+ const LEAF_COMPONENT_SIZES = {
23
+ idtimestamp: Urkle.LeafKeyBytes, // 8
24
+ valueBytes: Urkle.LeafValueBytes, // 32
25
+ extra1: Urkle.LeafExtra1Bytes, // 24
26
+ extra2: Urkle.LeafExtraBytes, // 32
27
+ extra3: Urkle.LeafExtraBytes, // 32
28
+ };
29
+ /**
30
+ * Returns the field index (for use with Massif.fieldref) of the start of the
31
+ * urkle leaf table data region.
32
+ *
33
+ * The field index is the offset in units of ValueBytes (32 bytes), which can be
34
+ * passed directly to Massif.fieldref() to get a view of the data.
35
+ *
36
+ * V2 layout before leaf table:
37
+ * StartHeader (256 bytes = 8 fields)
38
+ * IndexHeader (32 bytes = 1 field)
39
+ * BloomBitsets (4 * ceil(mBits/8))
40
+ * FrontierState (544 bytes)
41
+ *
42
+ * @param massifHeight - One-based massif height
43
+ * @returns Field index (in units of 32-byte fields) where the leaf table starts
44
+ */
45
+ export function urkleLeafTableStartFieldIndex(massifHeight) {
46
+ const leafCount = leafCountForMassifHeight(massifHeight);
47
+ const mBits = bloomMBits(leafCount);
48
+ const bloomBitsetsOnly = BigInt(Bloom.Filters) * bloomBitsetBytes(mBits > 0n ? mBits : 0n);
49
+ // Byte offset calculation
50
+ const startHeaderBytes = BigInt(LogFormat.StartHeaderSize); // 256
51
+ const indexHeaderBytes = BigInt(LogFormat.IndexHeaderBytes); // 32
52
+ const frontierBytes = BigInt(Urkle.FrontierStateV1Bytes); // 544
53
+ const byteOffset = startHeaderBytes + indexHeaderBytes + bloomBitsetsOnly + frontierBytes;
54
+ // Convert to field index (each field is 32 bytes)
55
+ // Note: this should be an exact division for valid massif heights
56
+ return Number(byteOffset / BigInt(LogFormat.ValueBytes));
57
+ }
58
+ /**
59
+ * Returns the byte offset within a massif buffer where the urkle leaf table
60
+ * starts. This is useful for direct byte-level access.
61
+ *
62
+ * @param massifHeight - One-based massif height
63
+ * @returns Byte offset where the leaf table starts
64
+ */
65
+ export function urkleLeafTableStartByteOffset(massifHeight) {
66
+ const leafCount = leafCountForMassifHeight(massifHeight);
67
+ const mBits = bloomMBits(leafCount);
68
+ const bloomBitsetsOnly = BigInt(Bloom.Filters) * bloomBitsetBytes(mBits > 0n ? mBits : 0n);
69
+ const startHeaderBytes = BigInt(LogFormat.StartHeaderSize);
70
+ const indexHeaderBytes = BigInt(LogFormat.IndexHeaderBytes);
71
+ const frontierBytes = BigInt(Urkle.FrontierStateV1Bytes);
72
+ return Number(startHeaderBytes + indexHeaderBytes + bloomBitsetsOnly + frontierBytes);
73
+ }
74
+ /**
75
+ * Creates an efficient leaf enumerator for iterating through urkle leaf data.
76
+ *
77
+ * The enumerator returns a generator function that yields LeafEntry objects
78
+ * for each leaf in the massif. Only the components specified in the spec
79
+ * are read and returned, minimizing overhead.
80
+ *
81
+ * The enumerator uses DataView for efficient typed access without copying.
82
+ *
83
+ * @param buffer - The massif buffer (Uint8Array)
84
+ * @param massifHeight - One-based massif height
85
+ * @param leafCount - Number of leaves to enumerate (from start position)
86
+ * @param spec - Specification of which components to read
87
+ * @param start - Starting leaf ordinal (0-based, defaults to 0)
88
+ * @returns Generator that yields LeafEntry objects
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * const enumerate = createLeafEnumerator(
93
+ * massif.buffer,
94
+ * massifHeight,
95
+ * actualLeafCount,
96
+ * { idtimestamp: true, valueBytes: true },
97
+ * 0 // start from first leaf
98
+ * );
99
+ *
100
+ * for (const leaf of enumerate()) {
101
+ * console.log(leaf.ordinal, leaf.idtimestamp, leaf.valueBytes);
102
+ * }
103
+ * ```
104
+ */
105
+ export function createLeafEnumerator(buffer, massifHeight, leafCount, spec, start = 0) {
106
+ const leafTableStart = urkleLeafTableStartByteOffset(massifHeight);
107
+ const recordBytes = Urkle.LeafRecordBytes;
108
+ // Pre-compute which components we need to read
109
+ const readIdtimestamp = spec.idtimestamp ?? false;
110
+ const readValueBytes = spec.valueBytes ?? false;
111
+ const readExtra1 = spec.extra1 ?? false;
112
+ const readExtra2 = spec.extra2 ?? false;
113
+ const readExtra3 = spec.extra3 ?? false;
114
+ // Create DataView for the buffer (reused for all reads)
115
+ const dataView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
116
+ return function* () {
117
+ for (let i = 0; i < leafCount; i++) {
118
+ const ordinal = start + i;
119
+ const recordOffset = leafTableStart + ordinal * recordBytes;
120
+ const entry = { ordinal };
121
+ if (readIdtimestamp) {
122
+ entry.idtimestamp = dataView.getBigUint64(recordOffset, false);
123
+ }
124
+ if (readValueBytes) {
125
+ entry.valueBytes = buffer.subarray(recordOffset + LEAF_COMPONENT_OFFSETS.valueBytes, recordOffset +
126
+ LEAF_COMPONENT_OFFSETS.valueBytes +
127
+ LEAF_COMPONENT_SIZES.valueBytes);
128
+ }
129
+ if (readExtra1) {
130
+ entry.extra1 = buffer.subarray(recordOffset + LEAF_COMPONENT_OFFSETS.extra1, recordOffset +
131
+ LEAF_COMPONENT_OFFSETS.extra1 +
132
+ LEAF_COMPONENT_SIZES.extra1);
133
+ }
134
+ if (readExtra2) {
135
+ entry.extra2 = buffer.subarray(recordOffset + LEAF_COMPONENT_OFFSETS.extra2, recordOffset +
136
+ LEAF_COMPONENT_OFFSETS.extra2 +
137
+ LEAF_COMPONENT_SIZES.extra2);
138
+ }
139
+ if (readExtra3) {
140
+ entry.extra3 = buffer.subarray(recordOffset + LEAF_COMPONENT_OFFSETS.extra3, recordOffset +
141
+ LEAF_COMPONENT_OFFSETS.extra3 +
142
+ LEAF_COMPONENT_SIZES.extra3);
143
+ }
144
+ yield entry;
145
+ }
146
+ };
147
+ }
148
+ /**
149
+ * Returns the byte offset of a specific leaf record's component.
150
+ *
151
+ * @param massifHeight - One-based massif height
152
+ * @param leafOrdinal - Zero-based leaf ordinal
153
+ * @param component - Which component to locate
154
+ * @returns Byte offset of the component within the massif buffer
155
+ */
156
+ export function leafComponentByteOffset(massifHeight, leafOrdinal, component) {
157
+ const leafTableStart = urkleLeafTableStartByteOffset(massifHeight);
158
+ const recordOffset = leafTableStart + leafOrdinal * Urkle.LeafRecordBytes;
159
+ return recordOffset + LEAF_COMPONENT_OFFSETS[component];
160
+ }
161
+ /**
162
+ * Returns the size in bytes of a leaf component.
163
+ *
164
+ * @param component - Which component
165
+ * @returns Size in bytes
166
+ */
167
+ export function leafComponentSize(component) {
168
+ return LEAF_COMPONENT_SIZES[component];
169
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Parse an R2 object path to extract log ID, massif height, and massif index.
3
+ *
4
+ * - v2/merklelog/massifs/{massifHeight}/{logId}/{massifIndex}.log
5
+ * - v2/merklelog/checkpoints/{massifHeight}/{logId}/{massifIndex}.sth
6
+ *
7
+ * @param path - The object path path from the R2 notification
8
+ * @returns Parsed components including logId, massifHeight, and massifIndex
9
+ * @throws Error if the path doesn't match expected format or if parsing fails
10
+ */
11
+ export declare function parseV2StorageObjectPath(path: string): {
12
+ logId: string;
13
+ massifHeight: number;
14
+ massifIndex: number;
15
+ type: string;
16
+ };
17
+ //# sourceMappingURL=v2storagepaths.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Parse an R2 object path to extract log ID, massif height, and massif index.
3
+ *
4
+ * - v2/merklelog/massifs/{massifHeight}/{logId}/{massifIndex}.log
5
+ * - v2/merklelog/checkpoints/{massifHeight}/{logId}/{massifIndex}.sth
6
+ *
7
+ * @param path - The object path path from the R2 notification
8
+ * @returns Parsed components including logId, massifHeight, and massifIndex
9
+ * @throws Error if the path doesn't match expected format or if parsing fails
10
+ */
11
+ export function parseV2StorageObjectPath(path) {
12
+ const parts = path.split("/");
13
+ if (parts.length < 6 || parts[0] !== "v2" || parts[1] !== "merklelog")
14
+ 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"
18
+ const massifHeightStr = parts[3];
19
+ const logId = parts[4];
20
+ const filename = parts[5];
21
+ // Validate extension
22
+ const expectedExt = typePart === "massifs" ? ".log" : ".sth";
23
+ if (!filename.endsWith(expectedExt)) {
24
+ throw new Error(`Expected ${expectedExt} extension for ${typePart}, got: ${filename}`);
25
+ }
26
+ // Parse massif index (hex, 16 digits)
27
+ 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}`);
30
+ }
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}`);
34
+ }
35
+ // Parse massifHeight
36
+ const massifHeight = Number.parseInt(massifHeightStr, 10);
37
+ if (!Number.isFinite(massifHeight)) {
38
+ throw new Error(`Failed to parse massif height as number: ${massifHeightStr}`);
39
+ }
40
+ return {
41
+ logId,
42
+ massifHeight,
43
+ massifIndex,
44
+ type: typePart,
45
+ };
46
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Convenience factory for a sync-backed Hasher (Node.js).
3
+ *
4
+ * createSyncHasher() returns a Hasher that uses node:crypto (SHA-256).
5
+ * digest() returns Promise.resolve(sync digest), so the hasher is suitable
6
+ * for callers who know they are in Node and want a sync-backed implementation.
7
+ */
8
+ import type { Hasher } from "./types.js";
9
+ /**
10
+ * Creates a Hasher that uses Node's crypto.createHash('sha256').
11
+ * digest() returns Promise.resolve(sync digest), so it is safe to use with
12
+ * when you need a sync-backed hasher (e.g. in Node).
13
+ *
14
+ * Only available in Node.js; throws if node:crypto is not available.
15
+ */
16
+ export declare function createSyncHasher(): Promise<Hasher>;
17
+ //# sourceMappingURL=algorithms-sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"algorithms-sync.d.ts","sourceRoot":"","sources":["../../src/mmr/algorithms-sync.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAazC;;;;;;GAMG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,MAAM,CAAC,CAiBxD"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Convenience factory for a sync-backed Hasher (Node.js).
3
+ *
4
+ * createSyncHasher() returns a Hasher that uses node:crypto (SHA-256).
5
+ * digest() returns Promise.resolve(sync digest), so the hasher is suitable
6
+ * for callers who know they are in Node and want a sync-backed implementation.
7
+ */
8
+ function concatChunks(chunks) {
9
+ const totalLength = chunks.reduce((s, c) => s + c.length, 0);
10
+ const out = new Uint8Array(totalLength);
11
+ let offset = 0;
12
+ for (const c of chunks) {
13
+ out.set(c, offset);
14
+ offset += c.length;
15
+ }
16
+ return out;
17
+ }
18
+ /**
19
+ * Creates a Hasher that uses Node's crypto.createHash('sha256').
20
+ * digest() returns Promise.resolve(sync digest), so it is safe to use with
21
+ * when you need a sync-backed hasher (e.g. in Node).
22
+ *
23
+ * Only available in Node.js; throws if node:crypto is not available.
24
+ */
25
+ export async function createSyncHasher() {
26
+ const { createHash } = await import("node:crypto");
27
+ const chunks = [];
28
+ return {
29
+ reset() {
30
+ chunks.length = 0;
31
+ },
32
+ update(data) {
33
+ chunks.push(data);
34
+ },
35
+ digest() {
36
+ const combined = concatChunks(chunks);
37
+ const buf = Buffer.from(combined);
38
+ const out = createHash("sha256").update(buf).digest();
39
+ return Promise.resolve(new Uint8Array(out));
40
+ },
41
+ };
42
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * MMR Algorithm Implementations
3
+ *
4
+ * Core algorithms for Merkle Mountain Range operations including
5
+ * peak bagging, inclusion proofs, and consistency proofs.
6
+ *
7
+ * This implementation is based on the authoritative Python reference:
8
+ * https://raw.githubusercontent.com/robinbryce/merkle-mountain-range-proofs/refs/heads/main/algorithms.py
9
+ *
10
+ * And the associated IETF draft specification:
11
+ * https://raw.githubusercontent.com/robinbryce/draft-bryce-cose-receipts-mmr-profile/refs/heads/main/draft-bryce-cose-receipts-mmr-profile.md
12
+ */
13
+ import type { Proof, Hasher } from "./types.js";
14
+ /**
15
+ * Bags peaks together to compute a single root hash.
16
+ *
17
+ * The root is defined as the 'bagging' of all peaks, starting with the highest.
18
+ * This creates a binary merkle tree from the peaks to obtain a single tree root.
19
+ *
20
+ * WARNING — NOT used by the receipt verification path, and NOT spec-aligned for
21
+ * MMRIVER receipts. The MMR profile
22
+ * (draft-bryce-cose-receipts-mmr-profile) proves inclusion to a single peak
23
+ * (the accumulator is the list of peaks); it does not bag peaks. This helper
24
+ * also hashes `H(right || left)` WITHOUT the 1-based position prefix that
25
+ * `calculateRoot` commits, so it is inconsistent with go-merklelog interior
26
+ * hashing. Retained only for any legacy bagging caller; do not introduce new
27
+ * consumers without first reconciling with the spec.
28
+ *
29
+ * @param hasher - Cryptographic hasher instance
30
+ * @param peaks - Array of peak hashes (highest to lowest)
31
+ * @returns The bagged root hash
32
+ */
33
+ export declare function bagPeaks(hasher: Hasher, peaks: Uint8Array[]): Promise<Uint8Array>;
34
+ /**
35
+ * Calculates the root hash from a leaf hash and inclusion proof
36
+ *
37
+ * Mirrors the reference `included_root` (algorithms.py): each interior node is
38
+ * `H(pos_BE8 || left || right)` where `pos` is the 1-based node position.
39
+ *
40
+ * @param hasher - Cryptographic hasher instance
41
+ * @param leafHash - Hash of the leaf being proven
42
+ * @param proof - Inclusion proof path
43
+ * @param leafIndex - The zero-based MMR index of the node being proven. Despite
44
+ * the name, this is treated as an MMR index (it seeds `currentPos = index + 1`
45
+ * and `heightIndex(index)`). For leaf 0 the leaf index and MMR index coincide;
46
+ * for any other leaf, callers MUST pass the MMR index (see `proof.mmrIndex`),
47
+ * not the leaf index.
48
+ * @returns The calculated root hash
49
+ */
50
+ export declare function calculateRoot(hasher: Hasher, leafHash: Uint8Array, proof: Proof, leafIndex: bigint): Promise<Uint8Array>;
51
+ /**
52
+ * Verifies an inclusion proof
53
+ *
54
+ * @param hasher - Cryptographic hasher instance
55
+ * @param leafHash - Hash of the leaf being proven
56
+ * @param proof - Inclusion proof
57
+ * @param root - Expected root hash
58
+ * @returns True if the proof is valid
59
+ */
60
+ export declare function verifyInclusion(hasher: Hasher, leafHash: Uint8Array, proof: Proof, root: Uint8Array): Promise<boolean>;
61
+ /**
62
+ * Verifies a consistency proof between two MMR states.
63
+ *
64
+ * WARNING — NOT IMPLEMENTED. This is a stub: it compares each root against
65
+ * itself and therefore ALWAYS returns true. It performs no real consistency
66
+ * check and MUST NOT be relied upon for security. No worker path currently
67
+ * calls it. See the reference `verify_consistency` (algorithms.py) for the
68
+ * intended algorithm (verify proof2 extends proof1 via position-committed
69
+ * interior hashing).
70
+ *
71
+ * @param hasher - Cryptographic hasher instance
72
+ * @param proof1 - Proof for the first state
73
+ * @param proof2 - Proof for the second state
74
+ * @param root1 - Root hash of the first state
75
+ * @param root2 - Root hash of the second state
76
+ * @returns Always true (stub) — do not use for verification
77
+ */
78
+ export declare function verifyConsistency(hasher: Hasher, proof1: Proof, proof2: Proof, root1: Uint8Array, root2: Uint8Array): boolean;
79
+ //# sourceMappingURL=algorithms.d.ts.map
@@ -0,0 +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;AAqBhD;;;;;;;;;;;;;;;;;;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;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,KAAK,EACb,MAAM,EAAE,KAAK,EACb,KAAK,EAAE,UAAU,EACjB,KAAK,EAAE,UAAU,GAChB,OAAO,CAKT"}
@@ -0,0 +1,155 @@
1
+ /**
2
+ * MMR Algorithm Implementations
3
+ *
4
+ * Core algorithms for Merkle Mountain Range operations including
5
+ * peak bagging, inclusion proofs, and consistency proofs.
6
+ *
7
+ * This implementation is based on the authoritative Python reference:
8
+ * https://raw.githubusercontent.com/robinbryce/merkle-mountain-range-proofs/refs/heads/main/algorithms.py
9
+ *
10
+ * And the associated IETF draft specification:
11
+ * https://raw.githubusercontent.com/robinbryce/draft-bryce-cose-receipts-mmr-profile/refs/heads/main/draft-bryce-cose-receipts-mmr-profile.md
12
+ */
13
+ import { Uint64 } from "../uint64/index.js";
14
+ import { heightIndex } from "./math.js";
15
+ import { arraysEqual } from "../utils/arrays.js";
16
+ /**
17
+ * Encodes a Uint64 as 8 bytes big-endian.
18
+ *
19
+ * Mirrors go-merklelog `HashWriteUint64` and the reference
20
+ * `pos.to_bytes(8, byteorder="big")` used by `hash_pospair64`.
21
+ */
22
+ function u64BigEndian(value) {
23
+ const out = new Uint8Array(8);
24
+ let v = value.toBigInt() & 0xffffffffffffffffn;
25
+ for (let i = 7; i >= 0; i--) {
26
+ out[i] = Number(v & 0xffn);
27
+ v >>= 8n;
28
+ }
29
+ return out;
30
+ }
31
+ /**
32
+ * Bags peaks together to compute a single root hash.
33
+ *
34
+ * The root is defined as the 'bagging' of all peaks, starting with the highest.
35
+ * This creates a binary merkle tree from the peaks to obtain a single tree root.
36
+ *
37
+ * WARNING — NOT used by the receipt verification path, and NOT spec-aligned for
38
+ * MMRIVER receipts. The MMR profile
39
+ * (draft-bryce-cose-receipts-mmr-profile) proves inclusion to a single peak
40
+ * (the accumulator is the list of peaks); it does not bag peaks. This helper
41
+ * also hashes `H(right || left)` WITHOUT the 1-based position prefix that
42
+ * `calculateRoot` commits, so it is inconsistent with go-merklelog interior
43
+ * hashing. Retained only for any legacy bagging caller; do not introduce new
44
+ * consumers without first reconciling with the spec.
45
+ *
46
+ * @param hasher - Cryptographic hasher instance
47
+ * @param peaks - Array of peak hashes (highest to lowest)
48
+ * @returns The bagged root hash
49
+ */
50
+ export async function bagPeaks(hasher, peaks) {
51
+ if (peaks.length === 0) {
52
+ throw new Error("Cannot bag empty peaks array");
53
+ }
54
+ if (peaks.length === 1) {
55
+ return peaks[0];
56
+ }
57
+ const peakHashes = [...peaks];
58
+ while (peakHashes.length > 1) {
59
+ const right = peakHashes.pop();
60
+ const left = peakHashes.pop();
61
+ hasher.reset();
62
+ hasher.update(right);
63
+ hasher.update(left);
64
+ const combined = await hasher.digest();
65
+ peakHashes.push(combined);
66
+ }
67
+ return peakHashes[0];
68
+ }
69
+ /**
70
+ * Calculates the root hash from a leaf hash and inclusion proof
71
+ *
72
+ * Mirrors the reference `included_root` (algorithms.py): each interior node is
73
+ * `H(pos_BE8 || left || right)` where `pos` is the 1-based node position.
74
+ *
75
+ * @param hasher - Cryptographic hasher instance
76
+ * @param leafHash - Hash of the leaf being proven
77
+ * @param proof - Inclusion proof path
78
+ * @param leafIndex - The zero-based MMR index of the node being proven. Despite
79
+ * the name, this is treated as an MMR index (it seeds `currentPos = index + 1`
80
+ * and `heightIndex(index)`). For leaf 0 the leaf index and MMR index coincide;
81
+ * for any other leaf, callers MUST pass the MMR index (see `proof.mmrIndex`),
82
+ * not the leaf index.
83
+ * @returns The calculated root hash
84
+ */
85
+ export async function calculateRoot(hasher, leafHash, proof, leafIndex) {
86
+ let currentHash = leafHash;
87
+ const mmrIndex = new Uint64(leafIndex);
88
+ let currentHeight = heightIndex(mmrIndex);
89
+ let currentPos = mmrIndex.add(new Uint64(1));
90
+ for (const siblingHash of proof.path) {
91
+ hasher.reset();
92
+ const nextHeight = heightIndex(new Uint64(currentPos.toBigInt()));
93
+ const isRightChild = nextHeight > currentHeight;
94
+ // Advance currentPos to the parent node's 1-based position, then commit it
95
+ // as the hash prefix: interior nodes are H(pos || left || right) per the
96
+ // MMR profile (draft-bryce-cose-receipts-mmr-profile `included_root` /
97
+ // `hash_pospair64`, and go-merklelog `HashPosPair64`). Omitting `pos`
98
+ // produces the wrong peak for any leaf above a single-leaf tree.
99
+ if (isRightChild) {
100
+ currentPos = currentPos.add(new Uint64(1));
101
+ hasher.update(u64BigEndian(currentPos));
102
+ hasher.update(siblingHash);
103
+ hasher.update(currentHash);
104
+ }
105
+ else {
106
+ currentPos = currentPos.add(new Uint64(2).shl(currentHeight));
107
+ hasher.update(u64BigEndian(currentPos));
108
+ hasher.update(currentHash);
109
+ hasher.update(siblingHash);
110
+ }
111
+ currentHash = await hasher.digest();
112
+ currentHeight += 1;
113
+ }
114
+ return currentHash;
115
+ }
116
+ /**
117
+ * Verifies an inclusion proof
118
+ *
119
+ * @param hasher - Cryptographic hasher instance
120
+ * @param leafHash - Hash of the leaf being proven
121
+ * @param proof - Inclusion proof
122
+ * @param root - Expected root hash
123
+ * @returns True if the proof is valid
124
+ */
125
+ export async function verifyInclusion(hasher, leafHash, proof, root) {
126
+ if (proof.leafIndex === undefined && proof.mmrIndex === undefined) {
127
+ throw new Error("Proof must have either leafIndex or mmrIndex");
128
+ }
129
+ const leafIdx = proof.leafIndex !== undefined ? proof.leafIndex : proof.mmrIndex;
130
+ const calculatedRoot = await calculateRoot(hasher, leafHash, proof, leafIdx);
131
+ return arraysEqual(calculatedRoot, root);
132
+ }
133
+ /**
134
+ * Verifies a consistency proof between two MMR states.
135
+ *
136
+ * WARNING — NOT IMPLEMENTED. This is a stub: it compares each root against
137
+ * itself and therefore ALWAYS returns true. It performs no real consistency
138
+ * check and MUST NOT be relied upon for security. No worker path currently
139
+ * calls it. See the reference `verify_consistency` (algorithms.py) for the
140
+ * intended algorithm (verify proof2 extends proof1 via position-committed
141
+ * interior hashing).
142
+ *
143
+ * @param hasher - Cryptographic hasher instance
144
+ * @param proof1 - Proof for the first state
145
+ * @param proof2 - Proof for the second state
146
+ * @param root1 - Root hash of the first state
147
+ * @param root2 - Root hash of the second state
148
+ * @returns Always true (stub) — do not use for verification
149
+ */
150
+ export function verifyConsistency(hasher, proof1, proof2, root1, root2) {
151
+ // TODO(plan-0027): Implement full consistency check (verify proof2 extends
152
+ // proof1) with position-committed interior hashing. Until then this returns
153
+ // true unconditionally and must not be used for any trust decision.
154
+ return arraysEqual(root1, root1) && arraysEqual(root2, root2);
155
+ }