@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
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@forestrie/merklelog",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "description": "MMR (merkle mountain range) proofs and massif storage format for Forestrie transparency logs",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/forestrie/canopy.git",
10
+ "directory": "packages/merklelog"
11
+ },
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js",
18
+ "default": "./dist/index.js"
19
+ }
20
+ },
21
+ "publishConfig": {
22
+ "registry": "https://registry.npmjs.org",
23
+ "access": "public"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src"
28
+ ],
29
+ "dependencies": {},
30
+ "devDependencies": {
31
+ "typescript": "^5.9.2",
32
+ "vitest": "^3.2.4"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.build.json",
36
+ "typecheck": "tsc --noEmit",
37
+ "test": "vitest --run",
38
+ "test:watch": "vitest",
39
+ "test:debug": "vitest --inspect --no-file-parallelism"
40
+ }
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * @forestrie/merklelog - TypeScript implementation of MMR Merklelog format
3
+ *
4
+ * This package provides TypeScript implementations of the MMR (Merkle Mountain Range)
5
+ * merklelog format as defined by the go-merklelog project.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ // Export uint64 module
11
+ export { Uint64 } from "./uint64/index.js";
12
+
13
+ // Export massifs module
14
+ export { Massif } from "./massifs/massif.js";
15
+ export type {
16
+ MassifStart,
17
+ LeafComponent,
18
+ LeafEnumeratorSpec,
19
+ LeafEntry,
20
+ } from "./massifs/types.js";
21
+ export { MassifStartFmt } from "./massifs/types.js";
22
+ export {
23
+ LogFormat,
24
+ Urkle,
25
+ Bloom,
26
+ IndexV2,
27
+ leafCountForMassifHeight,
28
+ leafTableBytes,
29
+ nodeCountMax,
30
+ nodeStoreBytes,
31
+ bloomMBits,
32
+ bloomBitsetBytes,
33
+ bloomRegionBytes,
34
+ indexDataBytesV2,
35
+ peakStackEnd,
36
+ massifLogEntries,
37
+ urkleLeafTableStartFieldIndex,
38
+ urkleLeafTableStartByteOffset,
39
+ createLeafEnumerator,
40
+ leafComponentByteOffset,
41
+ leafComponentSize,
42
+ computeLastMMRIndex,
43
+ isMassifFull,
44
+ } from "./massifs/types.js";
45
+
46
+ export { parseV2StorageObjectPath } from "./massifs/v2storagepaths.js";
47
+
48
+ // Export mmr module
49
+ export { mmrIndex, massifFirstLeaf, leafMinusSpurSum } from "./mmr/index.js";
50
+ export {
51
+ heightIndex,
52
+ height,
53
+ mmrIndexFromLeafIndex,
54
+ leafIndex,
55
+ mmrPosition,
56
+ mmrSizeFromHeightIndex,
57
+ leafCount,
58
+ leafCountFromHeightIndex,
59
+ } from "./mmr/math.js";
60
+ export {
61
+ bagPeaks,
62
+ calculateRoot,
63
+ verifyInclusion,
64
+ verifyConsistency,
65
+ } from "./mmr/algorithms.js";
66
+ export { createSyncHasher } from "./mmr/algorithms-sync.js";
67
+ export type { Proof, Peak } from "./mmr/types.js";
68
+ export type { Hasher } from "./mmr/types.js";
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Index format constants for v2 massif format
3
+ *
4
+ * Constants for the urkle trie and bloom filter index regions.
5
+ * Based on the Go implementations in:
6
+ * - go-merklelog/urkle/types.go
7
+ * - go-merklelog/bloom/types.go
8
+ * - go-merklelog/massifs/indexformat_v2.go
9
+ */
10
+
11
+ /**
12
+ * Urkle - Constants for urkle trie format
13
+ */
14
+ export namespace Urkle {
15
+ /** Fixed width of hashes and values (32 bytes) */
16
+ export const HashBytes = 32;
17
+
18
+ /** Byte width of leafOrdinal in proofs and leaf hashes */
19
+ export const LeafOrdinalBytes = 4;
20
+
21
+ /**
22
+ * LeafRecordBytes is the fixed byte width of a leaf table record.
23
+ *
24
+ * v1 layout (extended for Forestrie v2 index needs):
25
+ * - key_be8 (uint64, 8 bytes)
26
+ * - valueBytes[32] (committed by trie hash)
27
+ * - extra1[24] (auxiliary; not committed by the trie hash)
28
+ * - extra2[32] (auxiliary; not committed by the trie hash)
29
+ * - extra3[32] (auxiliary; not committed by the trie hash)
30
+ *
31
+ * NOTE: Record size is intentionally a multiple of 32 bytes. To achieve this
32
+ * without truncating valueBytes, we sacrifice 8 bytes from the first extra
33
+ * field.
34
+ */
35
+ export const LeafRecordBytes = 128; // 8 + 32 + 24 + 32 + 32
36
+
37
+ /** Fixed byte width of a node store record */
38
+ export const NodeRecordBytes = 64;
39
+
40
+ /** Size of FrontierStateV1 snapshot for resuming append-only construction */
41
+ export const FrontierStateV1Bytes = 544; // 32 + 64*8
42
+
43
+ // Leaf record field offsets
44
+ export const LeafKeyBytes = 8;
45
+ export const LeafValueBytes = HashBytes;
46
+ export const LeafExtra1Bytes = HashBytes - 8; // 24
47
+ export const LeafExtraBytes = HashBytes; // 32
48
+
49
+ export const LeafValueOffset = LeafKeyBytes; // 8
50
+ export const LeafExtraOffset = LeafValueOffset + LeafValueBytes; // 40
51
+ export const LeafExtra1Offset = LeafExtraOffset; // 40
52
+ export const LeafExtra2Offset = LeafExtra1Offset + LeafExtra1Bytes; // 64
53
+ export const LeafExtra3Offset = LeafExtra2Offset + LeafExtraBytes; // 96
54
+ }
55
+
56
+ /**
57
+ * Bloom - Constants for bloom filter format
58
+ */
59
+ export namespace Bloom {
60
+ /** Fixed element width (Forestrie log value width) */
61
+ export const ValueBytes = 32;
62
+
63
+ /** Number of parallel Bloom filters in this format */
64
+ export const Filters = 4;
65
+
66
+ /** Fixed header size for BloomHeaderV1 */
67
+ export const HeaderBytesV1 = 32;
68
+
69
+ /** Magic string for BloomHeaderV1 */
70
+ export const MagicV1 = "BLM1";
71
+
72
+ /** Version number for v1 */
73
+ export const VersionV1 = 1;
74
+
75
+ /** BitOrderLSB0: bit 0 is the least-significant bit of byte 0 */
76
+ export const BitOrderLSB0 = 0;
77
+ }
78
+
79
+ /**
80
+ * IndexV2 - V2 massif index configuration constants
81
+ */
82
+ export namespace IndexV2 {
83
+ /**
84
+ * BloomBitsPerElement is the fixed sizing knob for the v2 massif BloomRegion.
85
+ * mBits = bitsPerElement * leafCount, per filter.
86
+ */
87
+ export const BloomBitsPerElement = 10;
88
+
89
+ /**
90
+ * BloomK is the number of hash-derived bit positions set per inserted element.
91
+ * For b=10, k≈round(0.693*b)=7.
92
+ */
93
+ export const BloomK = 7;
94
+ }
95
+
96
+ /**
97
+ * Returns the fixed leaf capacity N for a massif height (one-based).
98
+ *
99
+ * In massifs, massifHeight is the one-based height h, so leaf capacity is:
100
+ * N = 2^(h-1)
101
+ *
102
+ * @param massifHeight - One-based massif height
103
+ * @returns Leaf capacity for the massif
104
+ */
105
+ export function leafCountForMassifHeight(massifHeight: number): bigint {
106
+ if (massifHeight === 0) {
107
+ return 0n;
108
+ }
109
+ return 1n << BigInt(massifHeight - 1);
110
+ }
111
+
112
+ /**
113
+ * Returns the required leaf table bytes for leafCount leaves.
114
+ *
115
+ * @param leafCount - Number of leaves
116
+ * @returns Byte size of the leaf table
117
+ */
118
+ export function leafTableBytes(leafCount: bigint): bigint {
119
+ return leafCount * BigInt(Urkle.LeafRecordBytes);
120
+ }
121
+
122
+ /**
123
+ * Returns the maximum number of nodes in a binary trie with leafCount keys.
124
+ * For a Patricia/crit-bit style binary trie, node count is <= 2N-1.
125
+ *
126
+ * @param leafCount - Number of leaves
127
+ * @returns Maximum node count
128
+ */
129
+ export function nodeCountMax(leafCount: bigint): bigint {
130
+ if (leafCount === 0n) {
131
+ return 0n;
132
+ }
133
+ return 2n * leafCount - 1n;
134
+ }
135
+
136
+ /**
137
+ * Returns the required node store bytes for leafCount leaves.
138
+ *
139
+ * @param leafCount - Number of leaves
140
+ * @returns Byte size of the node store
141
+ */
142
+ export function nodeStoreBytes(leafCount: bigint): bigint {
143
+ return nodeCountMax(leafCount) * BigInt(Urkle.NodeRecordBytes);
144
+ }
145
+
146
+ /**
147
+ * Computes mBits = bitsPerElement * leafCount, returning 0 if overflow.
148
+ *
149
+ * @param leafCount - Number of leaves
150
+ * @param bitsPerElement - Bits per element (default: IndexV2.BloomBitsPerElement)
151
+ * @returns mBits value, or 0 if overflow
152
+ */
153
+ export function bloomMBits(
154
+ leafCount: bigint,
155
+ bitsPerElement: bigint = BigInt(IndexV2.BloomBitsPerElement),
156
+ ): bigint {
157
+ const mBits = bitsPerElement * leafCount;
158
+ // Check overflow - mBits should fit in uint32
159
+ if (mBits > 0xffffffffn) {
160
+ return 0n;
161
+ }
162
+ return mBits;
163
+ }
164
+
165
+ /**
166
+ * Returns ceil(mBits/8) - the number of bytes for a single filter bitset.
167
+ *
168
+ * @param mBits - Number of bits
169
+ * @returns Byte size of one filter bitset
170
+ */
171
+ export function bloomBitsetBytes(mBits: bigint): bigint {
172
+ return (mBits + 7n) / 8n;
173
+ }
174
+
175
+ /**
176
+ * Returns the required byte length for a 4-way BloomRegion given mBits:
177
+ * HeaderBytesV1 + 4 * ceil(mBits/8)
178
+ *
179
+ * @param mBits - Number of bits per filter
180
+ * @returns Total byte size of bloom region (header + 4 bitsets)
181
+ */
182
+ export function bloomRegionBytes(mBits: bigint): bigint {
183
+ const bitsetBytes = bloomBitsetBytes(mBits);
184
+ return BigInt(Bloom.HeaderBytesV1) + BigInt(Bloom.Filters) * bitsetBytes;
185
+ }
186
+
187
+ /**
188
+ * Returns the byte size of the v2 index data region, excluding the fixed 32B
189
+ * index header.
190
+ *
191
+ * v2 index header (32B) is BloomHeaderV1, and the index data is:
192
+ * bloom bitsets || urkle frontier || urkle leaf table || urkle node store
193
+ *
194
+ * @param leafCount - Number of leaves
195
+ * @returns Byte size of index data region
196
+ * @throws Error if mBits overflows
197
+ */
198
+ export function indexDataBytesV2(leafCount: bigint): bigint {
199
+ const mBits = bloomMBits(leafCount);
200
+ if (mBits === 0n && leafCount > 0n) {
201
+ throw new Error("bloom mBits overflow");
202
+ }
203
+ if (leafCount === 0n) {
204
+ return 0n;
205
+ }
206
+
207
+ // Bloom region bytes includes the 32B header; we exclude that here because
208
+ // the massif index header is a fixed 32B already accounted for.
209
+ const bloomRegion = bloomRegionBytes(mBits);
210
+ const bloomBitsetsOnly = bloomRegion - BigInt(Bloom.HeaderBytesV1);
211
+
212
+ const frontierBytes = BigInt(Urkle.FrontierStateV1Bytes);
213
+ const ltBytes = leafTableBytes(leafCount);
214
+ const nsBytes = nodeStoreBytes(leafCount);
215
+
216
+ return bloomBitsetsOnly + frontierBytes + ltBytes + nsBytes;
217
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Massif log format constants
3
+ *
4
+ * Constants for calculating offsets and sizes in the massif blob format.
5
+ * Based on the Go implementation in go-merklelog/massifs/logformat.go
6
+ *
7
+ * Reference: https://github.com/datatrails/epic-8120-scalable-proof-mechanisms/blob/main/mmr/forestrie-mmrblobs.md#massif-basic-file-format
8
+ */
9
+
10
+ /**
11
+ * LogFormat - Constants for massif data format
12
+ *
13
+ * Matching the Go implementation in go-merklelog/massifs/logformat.go
14
+ */
15
+ export namespace LogFormat {
16
+ /**
17
+ * ValueBytes defines the width of ALL entries in the log. This fixed width
18
+ * makes it possible to compute mmr current sizes based on knowing only the
19
+ * massif height and the number of bytes in the file.
20
+ */
21
+ export const ValueBytes = 32;
22
+
23
+ /**
24
+ * ReservedHeaderSlots reserves a place to put the urkle trie root, used for
25
+ * data recovery and proofs of exclusion, and any related material. And it
26
+ * gives us a little flex in the data format for the initial launch of
27
+ * forestrie. It would be frustrating to need a data migration for want of a
28
+ * few bytes.
29
+ */
30
+ export const ReservedHeaderSlots = 7; // reserves n * ValueBytes at the front of the blob
31
+
32
+ /**
33
+ * StartHeaderSize - Size of the start header including reserved slots
34
+ */
35
+ export const StartHeaderSize = ValueBytes + ValueBytes * ReservedHeaderSlots; // 256
36
+
37
+ /**
38
+ * StartHeaderEnd - End offset of the start header
39
+ */
40
+ export const StartHeaderEnd = StartHeaderSize;
41
+
42
+ /**
43
+ * MaxMmrHeight - Maximum MMR height - no single log can be taller than this
44
+ * Matches the allowable bit size of an mmrIndex.
45
+ * Note that the max height *index* is 63
46
+ */
47
+ export const MaxMmrHeight = 64;
48
+
49
+ /**
50
+ * IndexHeaderBytes - Index header bytes - reserved space for index header.
51
+ * In v2, this 32B region is the BloomHeaderV1.
52
+ */
53
+ export const IndexHeaderBytes = 32;
54
+ }
@@ -0,0 +1,182 @@
1
+ import type { MassifStart } from "./massifstart.js";
2
+ import { MassifStartFmt } from "./massifstart.js";
3
+ import { LogFormat } from "./logformat.js";
4
+ import { massifFirstLeaf, leafMinusSpurSum } from "../mmr/index.js";
5
+
6
+ /**
7
+ * Massif - Efficient buffer-based access to massif data
8
+ *
9
+ * Provides efficient access to massif start data based on a view of an
10
+ * in-memory buffer which holds the data. All operations work directly with
11
+ * the buffer without copying.
12
+ */
13
+ export class Massif {
14
+ private readonly buffer: Uint8Array;
15
+
16
+ /**
17
+ * Creates a new Massif instance from buffer data
18
+ * @param data - The massif data as ArrayBuffer, Uint8Array, or Buffer
19
+ */
20
+ constructor(
21
+ data:
22
+ | ArrayBuffer
23
+ | Uint8Array
24
+ | Buffer
25
+ | { buffer: ArrayBuffer; byteOffset: number; byteLength: number },
26
+ ) {
27
+ if (data instanceof ArrayBuffer) {
28
+ this.buffer = new Uint8Array(data);
29
+ } else if (data instanceof Uint8Array) {
30
+ this.buffer = data;
31
+ } else if (
32
+ "buffer" in data &&
33
+ "byteOffset" in data &&
34
+ "byteLength" in data
35
+ ) {
36
+ // Node.js Buffer or similar
37
+ this.buffer = new Uint8Array(
38
+ data.buffer,
39
+ data.byteOffset,
40
+ data.byteLength,
41
+ );
42
+ } else {
43
+ throw new Error("Unsupported buffer type");
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Returns a MassifStart instance populated by reading directly from the buffer
49
+ *
50
+ * Computes firstIndex using massif height and index (calls MMR function)
51
+ * Computes peakStackLen using massif index (calls MMR function)
52
+ */
53
+ getStart(): MassifStart {
54
+ const massifHeight = this.massifHeight;
55
+ const massifIndex = this.massifIndex;
56
+
57
+ // Compute firstIndex using massif height and index
58
+ const firstIndex = massifFirstLeaf(massifHeight, massifIndex);
59
+
60
+ // Compute peakStackLen using massif index
61
+ const peakStackLen = leafMinusSpurSum(BigInt(massifIndex));
62
+
63
+ return {
64
+ reserved: this.reserved,
65
+ lastID: this.lastID,
66
+ version: this.version,
67
+ commitmentEpoch: this.commitmentEpoch,
68
+ massifHeight,
69
+ massifIndex,
70
+ firstIndex,
71
+ peakStackLen,
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Returns a buffer view of the corresponding field at zero-based index
77
+ *
78
+ * Each field is 32 bytes (LogFormat.ValueBytes). Zero-based indexing, without regard
79
+ * to peakstack or trieindex structure.
80
+ *
81
+ * @param index - Zero-based field index (Number or BigInt)
82
+ * @param count - Number of consecutive fields to include in the view (defaults to 1)
83
+ * @returns Uint8Array view of the field(s) (32 bytes per field)
84
+ */
85
+ fieldref(index: number | bigint, count: number = 1): Uint8Array {
86
+ const idx = typeof index === "bigint" ? Number(index) : index;
87
+ const offset = idx * LogFormat.ValueBytes;
88
+ return this.buffer.slice(offset, offset + count * LogFormat.ValueBytes);
89
+ }
90
+
91
+ /**
92
+ * Reads bytes from the buffer at the specified offset
93
+ *
94
+ * Returns a view of the buffer without copying. Efficient for reading
95
+ * arbitrary byte ranges that may span multiple 32-byte fields.
96
+ *
97
+ * @param offset - Byte offset into the buffer
98
+ * @param length - Number of bytes to read
99
+ * @returns Uint8Array view of the requested bytes
100
+ */
101
+ readBytes(offset: number, length: number): Uint8Array {
102
+ return this.buffer.slice(offset, offset + length);
103
+ }
104
+
105
+ /**
106
+ * Reads a big-endian uint64 from the buffer
107
+ */
108
+ private readUint64BE(offset: number): bigint {
109
+ const view = new DataView(
110
+ this.buffer.buffer,
111
+ this.buffer.byteOffset + offset,
112
+ 8,
113
+ );
114
+ return view.getBigUint64(0, false); // false = big-endian
115
+ }
116
+
117
+ /**
118
+ * Reads a big-endian uint32 from the buffer
119
+ */
120
+ private readUint32BE(offset: number): number {
121
+ const view = new DataView(
122
+ this.buffer.buffer,
123
+ this.buffer.byteOffset + offset,
124
+ 4,
125
+ );
126
+ return view.getUint32(0, false); // false = big-endian
127
+ }
128
+
129
+ /**
130
+ * Reads a big-endian uint16 from the buffer
131
+ */
132
+ private readUint16BE(offset: number): number {
133
+ const view = new DataView(
134
+ this.buffer.buffer,
135
+ this.buffer.byteOffset + offset,
136
+ 2,
137
+ );
138
+ return view.getUint16(0, false); // false = big-endian
139
+ }
140
+
141
+ /**
142
+ * Reserved field (bytes 0-7)
143
+ */
144
+ get reserved(): bigint {
145
+ return this.readUint64BE(0);
146
+ }
147
+
148
+ /**
149
+ * Last ID timestamp (bytes 8-15)
150
+ */
151
+ get lastID(): bigint {
152
+ return this.readUint64BE(MassifStartFmt.LastIdFirstByte);
153
+ }
154
+
155
+ /**
156
+ * Version (bytes 21-22)
157
+ */
158
+ get version(): number {
159
+ return this.readUint16BE(MassifStartFmt.VersionFirstByte);
160
+ }
161
+
162
+ /**
163
+ * Commitment epoch (bytes 23-26)
164
+ */
165
+ get commitmentEpoch(): number {
166
+ return this.readUint32BE(MassifStartFmt.EpochFirstByte);
167
+ }
168
+
169
+ /**
170
+ * Massif height (byte 27)
171
+ */
172
+ get massifHeight(): number {
173
+ return this.buffer[MassifStartFmt.MassifHeightFirstByte];
174
+ }
175
+
176
+ /**
177
+ * Massif index (bytes 28-31)
178
+ */
179
+ get massifIndex(): number {
180
+ return this.readUint32BE(MassifStartFmt.MassifFirstByte);
181
+ }
182
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Massif fullness determination functions
3
+ *
4
+ * Functions for determining if a massif is full based on its height and log entries.
5
+ */
6
+
7
+ /**
8
+ * Determine if a massif is full.
9
+ *
10
+ * @param massifHeight - Massif height (1-based)
11
+ * @param logEntries - Number of log entries (nodes) in the massif
12
+ * @returns True if the massif is full
13
+ */
14
+ export function isMassifFull(
15
+ massifHeight: number,
16
+ logEntries: bigint,
17
+ ): boolean {
18
+ // Convert 1-based height to 0-based height index
19
+ const heightIndex = massifHeight - 1;
20
+
21
+ // Expected number of leaves for a full massif: f = 2^g = 1 << heightIndex
22
+ const expectedLeaves = BigInt(1 << heightIndex);
23
+
24
+ // Actual number of leaves: f = (n + 1) / 2
25
+ const actualLeaves = (logEntries + 1n) >> 1n;
26
+
27
+ return actualLeaves >= expectedLeaves;
28
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * MassifLogEntries calculation for v2 massif format
3
+ */
4
+
5
+ import { LogFormat } from "./logformat.js";
6
+ import { peakStackEnd } from "./peakstackend.js";
7
+
8
+ /**
9
+ * MassifLogEntries calculates the number of log entries (nodes) in a blob from
10
+ * the length of the blob in bytes.
11
+ *
12
+ * It does this by accounting for the index data and other header data.
13
+ * If you know the FirstIndex from the massif start header you can get the
14
+ * overall mmr size by direct addition.
15
+ *
16
+ * Note: this function exists so we can compute the mmrSize from just the blob
17
+ * store metadata: we store the FirstIndex on a blob tag, and the blob
18
+ * metadata includes ContentLength. This means when we are checking if a root
19
+ * seal covers the current log head, we don't need to fetch the log massif blob.
20
+ *
21
+ * @param dataLen - Total length of the massif blob in bytes
22
+ * @param massifHeight - One-based massif height
23
+ * @returns Number of log entries (nodes) in the blob
24
+ * @throws Error if the data length is too short to contain the required headers
25
+ */
26
+ export function massifLogEntries(
27
+ dataLen: number,
28
+ massifHeight: number,
29
+ ): bigint {
30
+ const stackEnd = peakStackEnd(massifHeight);
31
+
32
+ if (BigInt(dataLen) < stackEnd) {
33
+ throw new Error(
34
+ `Massif data length ${dataLen} is too short. Minimum required: ${stackEnd} bytes`,
35
+ );
36
+ }
37
+
38
+ const mmrByteCount = BigInt(dataLen) - stackEnd;
39
+ return mmrByteCount / BigInt(LogFormat.ValueBytes);
40
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * MassifStart - Header information for a massif blob
3
+ *
4
+ * The massif start is a 32-byte field encoding bookkeeping information
5
+ * required in a blob to allow for efficient correctness checks.
6
+ */
7
+ export interface MassifStart {
8
+ /** Reserved field (bytes 0-7) */
9
+ reserved: bigint;
10
+ /** Last ID timestamp (bytes 8-15) */
11
+ lastID: bigint;
12
+ /** Version (bytes 21-22) */
13
+ version: number;
14
+ /** Commitment epoch (bytes 23-26) */
15
+ commitmentEpoch: number;
16
+ /** Massif height (byte 27) */
17
+ massifHeight: number;
18
+ /** Massif index (bytes 28-31) */
19
+ massifIndex: number;
20
+ /** First MMR index in this massif (computed) */
21
+ firstIndex: bigint;
22
+ /** Peak stack length (computed) */
23
+ peakStackLen: bigint;
24
+ }
25
+
26
+ /**
27
+ * MassifStartFmt - Format constants for MassifStart fields
28
+ *
29
+ * Byte offsets and sizes for MassifStart fields.
30
+ * Matching the Go implementation in go-merklelog/massifs/massifstart.go
31
+ */
32
+ export namespace MassifStartFmt {
33
+ /** Last ID timestamp field - first byte offset */
34
+ export const LastIdFirstByte = 8;
35
+ /** Last ID timestamp field - size in bytes */
36
+ export const LastIdSize = 8;
37
+ /** Last ID timestamp field - end byte offset */
38
+ export const LastIdEnd = LastIdFirstByte + LastIdSize;
39
+
40
+ /** Version field - first byte offset */
41
+ export const VersionFirstByte = 21;
42
+ /** Version field - size in bytes */
43
+ export const VersionSize = 2;
44
+ /** Version field - end byte offset */
45
+ export const VersionEnd = VersionFirstByte + VersionSize;
46
+
47
+ /** Commitment epoch field - first byte offset */
48
+ export const EpochFirstByte = VersionEnd; // 23
49
+ /** Commitment epoch field - size in bytes */
50
+ export const EpochSize = 4;
51
+ /** Commitment epoch field - end byte offset */
52
+ export const EpochEnd = EpochFirstByte + EpochSize;
53
+
54
+ /** Massif height field - first byte offset */
55
+ export const MassifHeightFirstByte = EpochEnd; // 27
56
+ /** Massif height field - size in bytes */
57
+ export const MassifHeightSize = 1;
58
+ /** Massif height field - end byte offset */
59
+ export const MassifHeightEnd = MassifHeightFirstByte + MassifHeightSize;
60
+
61
+ /** Massif index field - first byte offset */
62
+ export const MassifFirstByte = MassifHeightEnd; // 28
63
+ /** Massif index field - size in bytes */
64
+ export const MassifSize = 4;
65
+ /** Massif index field - end byte offset */
66
+ export const MassifEnd = MassifFirstByte + MassifSize;
67
+ }