@z-torrent/merkle-tree 0.0.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Z-Torrent contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @z-torrent/merkle-tree
2
+
3
+ [BEP 52](http://www.bittorrent.org/beps/bep_0052.html) BitTorrent v2 **merkle hash trees** (SHA-256, 16 KiB leaf blocks). Used by [@z-torrent/create](https://www.npmjs.com/package/@z-torrent/create) and [@z-torrent/core](https://www.npmjs.com/package/@z-torrent/core) for v2 / hybrid torrents.
4
+
5
+ **Node.js 18+** (uses `node:crypto`).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @z-torrent/merkle-tree
11
+ ```
12
+
13
+ ## API overview
14
+
15
+ ### Constants
16
+
17
+ - **`BEP52_BLOCK_SIZE`** — leaf block size (16 KiB).
18
+ - **`BEP52_ZERO_LEAF`** — 32 zero bytes; padding leaf per BEP 52.
19
+
20
+ ### Hash helpers
21
+
22
+ - **`sha256Data(data)`** — SHA-256 of raw bytes.
23
+ - **`sha256Concat(left, right)`** — SHA-256 of `left || right` (64-byte node input).
24
+
25
+ ### Layers and roots
26
+
27
+ - **`rootHashLayer(hashes)`** — Merkle root of a complete binary layer (length must be a power of two). Each parent is `SHA256(left || right)`.
28
+ - **`buildMerkleLayers(leaves)`** — Build all tree layers from leaf hashes (padded to the next power of two with zero leaves).
29
+ - **`padPieceBlockHashes(blockHashes, blocksPerPiece, isFirstPiece)`** — Pad per-piece block hashes to the leaf count required by BEP 52.
30
+ - **`padPieceRoot(blocksPerPiece)`** — Root of a virtual “all zero leaf” piece (file-tree balancing).
31
+
32
+ ### File hashing (torrent creator)
33
+
34
+ - **`buildFileV2Merkle(fileData, pieceLength)`** — Compute v2 per-file metadata: `piecesRoot`, optional `pieceLayerConcat`, and `pieceSubtreeRoots`. `pieceLength` must be a power of two and ≥ 16 KiB.
35
+
36
+ ### Proofs and verification
37
+
38
+ - **`unclesForLeafIndex(layers, leafIndex, proofLayers)`** — Uncle (sibling) hashes for a leaf, bottom-up.
39
+ - **`verifyLeafToRoot(leafHash, leafIndex, uncles, expectedRoot)`** — Check a Merkle path.
40
+ - **`pieceSubtreeRootFromBytes(pieceBytes, pieceLength, isFirstPieceOfFile)`** — Subtree root for one logical piece’s raw bytes (matches reference BEP 52 padding for the first piece of a file).
41
+
42
+ ### Types
43
+
44
+ - **`FileV2MerkleResult`** — `{ length, piecesRoot?, pieceLayerConcat?, pieceSubtreeRoots }`.
45
+
46
+ ## Example
47
+
48
+ ```js
49
+ import { buildFileV2Merkle, BEP52_BLOCK_SIZE } from '@z-torrent/merkle-tree'
50
+
51
+ const data = new TextEncoder().encode('hello')
52
+ const pieceLength = 32 * 1024 // ≥ BEP52_BLOCK_SIZE, power of two
53
+ const { piecesRoot, pieceSubtreeRoots } = buildFileV2Merkle(data, pieceLength)
54
+ ```
55
+
56
+ ## License
57
+
58
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,58 @@
1
+ //#region src/bep52.d.ts
2
+ /** 16 KiB — BEP 52 leaf block size */
3
+ declare const BEP52_BLOCK_SIZE: number;
4
+ /** 32 zero bytes — padding leaf per BEP 52 */
5
+ declare const BEP52_ZERO_LEAF: Uint8Array<ArrayBuffer>;
6
+ declare function sha256Concat(left: Uint8Array, right: Uint8Array): Uint8Array;
7
+ declare function sha256Data(data: Uint8Array): Uint8Array;
8
+ /**
9
+ * Merkle root of a complete binary layer (length must be a power of two).
10
+ * Each node is SHA256(left || right).
11
+ */
12
+ declare function rootHashLayer(hashes: Uint8Array[]): Uint8Array;
13
+ /**
14
+ * Pad block hashes for one piece to the leaf count required by BEP 52.
15
+ */
16
+ declare function padPieceBlockHashes(blockHashes: Uint8Array[], blocksPerPiece: number, isFirstPiece: boolean): Uint8Array[];
17
+ /** Root of a “virtual” piece made entirely of zero leaves (BEP 52 file-tree balancing). */
18
+ declare function padPieceRoot(blocksPerPiece: number): Uint8Array;
19
+ interface FileV2MerkleResult {
20
+ /** Total file length in bytes */
21
+ length: number;
22
+ /** Merkle root over piece-level hashes (`pieces root` in file tree) */
23
+ piecesRoot?: Uint8Array;
24
+ /**
25
+ * Concatenated 32-byte piece subtree roots for `piece layers` dict value.
26
+ * Omitted when file fits in one piece (no `piece layers` entry per BEP 52).
27
+ */
28
+ pieceLayerConcat?: Uint8Array;
29
+ /** Per-piece subtree roots (each is root of that piece’s block merkle tree) */
30
+ pieceSubtreeRoots: Uint8Array[];
31
+ }
32
+ /**
33
+ * Build BEP 52 per-file merkle metadata from raw file bytes.
34
+ * Mirrors `FileHasher` in the reference `bep_0052_torrent_creator.py`.
35
+ */
36
+ declare function buildFileV2Merkle(fileData: Uint8Array, pieceLength: number): FileV2MerkleResult;
37
+ /**
38
+ * Build all layers from leaf hashes (padded to power of two with zero leaves).
39
+ */
40
+ declare function buildMerkleLayers(leaves: Uint8Array[]): Uint8Array[][];
41
+ /**
42
+ * Uncle hashes for one leaf index, from leaf layer up `proofLayers` steps.
43
+ * Returns uncles from bottom to top (closest to root last).
44
+ */
45
+ declare function unclesForLeafIndex(layers: Uint8Array[][], leafIndex: number, proofLayers: number): Uint8Array[];
46
+ /**
47
+ * Verify that `leafHash` at `leafIndex` (in padded leaf layer) reaches `expectedRoot`
48
+ * using `uncles` (sibling hashes bottom-up).
49
+ */
50
+ /**
51
+ * Merkle subtree root for one logical piece’s raw bytes (BEP 52).
52
+ * `isFirstPieceOfFile` matches reference `FileHasher` padding (first piece of each file).
53
+ */
54
+ declare function pieceSubtreeRootFromBytes(pieceBytes: Uint8Array, pieceLength: number, isFirstPieceOfFile: boolean): Uint8Array;
55
+ declare function verifyLeafToRoot(leafHash: Uint8Array, leafIndex: number, uncles: Uint8Array[], expectedRoot: Uint8Array): boolean;
56
+ //#endregion
57
+ export { BEP52_BLOCK_SIZE, BEP52_ZERO_LEAF, type FileV2MerkleResult, buildFileV2Merkle, buildMerkleLayers, padPieceBlockHashes, padPieceRoot, pieceSubtreeRootFromBytes, rootHashLayer, sha256Concat, sha256Data, unclesForLeafIndex, verifyLeafToRoot };
58
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,176 @@
1
+ import { createHash } from "node:crypto";
2
+ //#region src/bep52.ts
3
+ /*! @z-torrent/merkle-tree. MIT License. */
4
+ /** 16 KiB — BEP 52 leaf block size */
5
+ const BEP52_BLOCK_SIZE = 16384;
6
+ /** 32 zero bytes — padding leaf per BEP 52 */
7
+ const BEP52_ZERO_LEAF = new Uint8Array(32);
8
+ function sha256Concat(left, right) {
9
+ const buf = new Uint8Array(64);
10
+ buf.set(left, 0);
11
+ buf.set(right, 32);
12
+ return new Uint8Array(createHash("sha256").update(buf).digest());
13
+ }
14
+ function sha256Data(data) {
15
+ return new Uint8Array(createHash("sha256").update(data).digest());
16
+ }
17
+ /**
18
+ * Merkle root of a complete binary layer (length must be a power of two).
19
+ * Each node is SHA256(left || right).
20
+ */
21
+ function rootHashLayer(hashes) {
22
+ if (hashes.length === 0) throw new Error("rootHashLayer: empty layer");
23
+ if ((hashes.length & hashes.length - 1) !== 0) throw new Error("rootHashLayer: layer length must be a power of two");
24
+ let layer = hashes;
25
+ while (layer.length > 1) {
26
+ const next = [];
27
+ for (let i = 0; i < layer.length; i += 2) next.push(sha256Concat(layer[i], layer[i + 1]));
28
+ layer = next;
29
+ }
30
+ return layer[0];
31
+ }
32
+ function nextPow2(n) {
33
+ if (n <= 1) return 1;
34
+ return 1 << 32 - Math.clz32(n - 1);
35
+ }
36
+ /** Like Python `int.bit_length()` for non-negative integers. */
37
+ function uintBitLength(x) {
38
+ if (x <= 0) return 0;
39
+ return 32 - Math.clz32(x);
40
+ }
41
+ /**
42
+ * Pad block hashes for one piece to the leaf count required by BEP 52.
43
+ */
44
+ function padPieceBlockHashes(blockHashes, blocksPerPiece, isFirstPiece) {
45
+ const n = blockHashes.length;
46
+ if (n === 0) return [];
47
+ const leavesRequired = isFirstPiece ? 1 << uintBitLength(n - 1) : blocksPerPiece;
48
+ const out = blockHashes.slice();
49
+ while (out.length < leavesRequired) out.push(BEP52_ZERO_LEAF.slice());
50
+ return out;
51
+ }
52
+ /** Root of a “virtual” piece made entirely of zero leaves (BEP 52 file-tree balancing). */
53
+ function padPieceRoot(blocksPerPiece) {
54
+ const zeros = [];
55
+ for (let i = 0; i < blocksPerPiece; i++) zeros.push(BEP52_ZERO_LEAF.slice());
56
+ return rootHashLayer(zeros);
57
+ }
58
+ /**
59
+ * Build BEP 52 per-file merkle metadata from raw file bytes.
60
+ * Mirrors `FileHasher` in the reference `bep_0052_torrent_creator.py`.
61
+ */
62
+ function buildFileV2Merkle(fileData, pieceLength) {
63
+ if (pieceLength < 16384 || (pieceLength & pieceLength - 1) !== 0) throw new Error("pieceLength must be a power of two and >= 16 KiB");
64
+ const blocksPerPiece = pieceLength / BEP52_BLOCK_SIZE;
65
+ const pieceSubtreeRoots = [];
66
+ let offset = 0;
67
+ const len = fileData.length;
68
+ while (offset < len) {
69
+ const blockHashes = [];
70
+ for (let i = 0; i < blocksPerPiece && offset < len; i++) {
71
+ const end = Math.min(offset + BEP52_BLOCK_SIZE, len);
72
+ const block = fileData.subarray(offset, end);
73
+ if (block.length === 0) break;
74
+ offset = end;
75
+ blockHashes.push(sha256Data(block));
76
+ }
77
+ if (blockHashes.length === 0) break;
78
+ const padded = padPieceBlockHashes(blockHashes, blocksPerPiece, pieceSubtreeRoots.length === 0);
79
+ pieceSubtreeRoots.push(rootHashLayer(padded));
80
+ }
81
+ const length = len;
82
+ if (length === 0) return {
83
+ length: 0,
84
+ pieceSubtreeRoots: []
85
+ };
86
+ const layerHashes = pieceSubtreeRoots.slice();
87
+ let pieceLayerConcat;
88
+ if (pieceSubtreeRoots.length > 1) {
89
+ const flat = new Uint8Array(pieceSubtreeRoots.length * 32);
90
+ let p = 0;
91
+ for (const h of pieceSubtreeRoots) {
92
+ flat.set(h, p);
93
+ p += 32;
94
+ }
95
+ pieceLayerConcat = flat;
96
+ }
97
+ const padPiece = padPieceRoot(blocksPerPiece);
98
+ const targetPieces = nextPow2(layerHashes.length);
99
+ while (layerHashes.length < targetPieces) layerHashes.push(padPiece.slice());
100
+ const out = {
101
+ length,
102
+ piecesRoot: rootHashLayer(layerHashes),
103
+ pieceSubtreeRoots
104
+ };
105
+ if (length > pieceLength && pieceLayerConcat) out.pieceLayerConcat = pieceLayerConcat;
106
+ return out;
107
+ }
108
+ /**
109
+ * Build all layers from leaf hashes (padded to power of two with zero leaves).
110
+ */
111
+ function buildMerkleLayers(leaves) {
112
+ if (leaves.length === 0) throw new Error("buildMerkleLayers: no leaves");
113
+ const n = nextPow2(leaves.length);
114
+ const padded = leaves.slice();
115
+ while (padded.length < n) padded.push(BEP52_ZERO_LEAF.slice());
116
+ const layers = [padded];
117
+ let layer = padded;
118
+ while (layer.length > 1) {
119
+ const next = [];
120
+ for (let i = 0; i < layer.length; i += 2) next.push(sha256Concat(layer[i], layer[i + 1]));
121
+ layers.push(next);
122
+ layer = next;
123
+ }
124
+ return layers;
125
+ }
126
+ /**
127
+ * Uncle hashes for one leaf index, from leaf layer up `proofLayers` steps.
128
+ * Returns uncles from bottom to top (closest to root last).
129
+ */
130
+ function unclesForLeafIndex(layers, leafIndex, proofLayers) {
131
+ const uncles = [];
132
+ let idx = leafIndex;
133
+ for (let depth = 0; depth < proofLayers && depth < layers.length - 1; depth++) {
134
+ const row = layers[depth];
135
+ const sibling = idx ^ 1;
136
+ if (sibling >= row.length) break;
137
+ uncles.push(row[sibling].slice());
138
+ idx >>= 1;
139
+ }
140
+ return uncles;
141
+ }
142
+ /**
143
+ * Verify that `leafHash` at `leafIndex` (in padded leaf layer) reaches `expectedRoot`
144
+ * using `uncles` (sibling hashes bottom-up).
145
+ */
146
+ /**
147
+ * Merkle subtree root for one logical piece’s raw bytes (BEP 52).
148
+ * `isFirstPieceOfFile` matches reference `FileHasher` padding (first piece of each file).
149
+ */
150
+ function pieceSubtreeRootFromBytes(pieceBytes, pieceLength, isFirstPieceOfFile) {
151
+ const blocksPerPiece = pieceLength / BEP52_BLOCK_SIZE;
152
+ const blockHashes = [];
153
+ let offset = 0;
154
+ while (offset < pieceBytes.length) {
155
+ const end = Math.min(offset + BEP52_BLOCK_SIZE, pieceBytes.length);
156
+ blockHashes.push(sha256Data(pieceBytes.subarray(offset, end)));
157
+ offset = end;
158
+ }
159
+ if (blockHashes.length === 0) return rootHashLayer([BEP52_ZERO_LEAF.slice()]);
160
+ return rootHashLayer(padPieceBlockHashes(blockHashes, blocksPerPiece, isFirstPieceOfFile));
161
+ }
162
+ function verifyLeafToRoot(leafHash, leafIndex, uncles, expectedRoot) {
163
+ let idx = leafIndex;
164
+ let acc = leafHash.slice();
165
+ for (const uncle of uncles) {
166
+ acc = sha256Concat((idx & 1) === 0 ? acc : uncle, (idx & 1) === 0 ? uncle : acc);
167
+ idx >>= 1;
168
+ }
169
+ if (acc.length !== expectedRoot.length) return false;
170
+ for (let i = 0; i < acc.length; i++) if (acc[i] !== expectedRoot[i]) return false;
171
+ return true;
172
+ }
173
+ //#endregion
174
+ export { BEP52_BLOCK_SIZE, BEP52_ZERO_LEAF, buildFileV2Merkle, buildMerkleLayers, padPieceBlockHashes, padPieceRoot, pieceSubtreeRootFromBytes, rootHashLayer, sha256Concat, sha256Data, unclesForLeafIndex, verifyLeafToRoot };
175
+
176
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/bep52.ts"],"sourcesContent":["/*! @z-torrent/merkle-tree. MIT License. */\nimport { createHash } from 'node:crypto'\n\n/** 16 KiB — BEP 52 leaf block size */\nexport const BEP52_BLOCK_SIZE = 1 << 14\n\n/** 32 zero bytes — padding leaf per BEP 52 */\nexport const BEP52_ZERO_LEAF = new Uint8Array(32)\n\nexport function sha256Concat(left: Uint8Array, right: Uint8Array): Uint8Array {\n const buf = new Uint8Array(64)\n buf.set(left, 0)\n buf.set(right, 32)\n return new Uint8Array(createHash('sha256').update(buf).digest())\n}\n\nexport function sha256Data(data: Uint8Array): Uint8Array {\n return new Uint8Array(createHash('sha256').update(data).digest())\n}\n\n/**\n * Merkle root of a complete binary layer (length must be a power of two).\n * Each node is SHA256(left || right).\n */\nexport function rootHashLayer(hashes: Uint8Array[]): Uint8Array {\n if (hashes.length === 0) {\n throw new Error('rootHashLayer: empty layer')\n }\n if ((hashes.length & (hashes.length - 1)) !== 0) {\n throw new Error('rootHashLayer: layer length must be a power of two')\n }\n let layer = hashes\n while (layer.length > 1) {\n const next: Uint8Array[] = []\n for (let i = 0; i < layer.length; i += 2) {\n next.push(sha256Concat(layer[i]!, layer[i + 1]!))\n }\n layer = next\n }\n return layer[0]!\n}\n\nfunction nextPow2(n: number): number {\n if (n <= 1) return 1\n return 1 << (32 - Math.clz32(n - 1))\n}\n\n/** Like Python `int.bit_length()` for non-negative integers. */\nfunction uintBitLength(x: number): number {\n if (x <= 0) return 0\n return 32 - Math.clz32(x)\n}\n\n/**\n * Pad block hashes for one piece to the leaf count required by BEP 52.\n */\nexport function padPieceBlockHashes(\n blockHashes: Uint8Array[],\n blocksPerPiece: number,\n isFirstPiece: boolean\n): Uint8Array[] {\n const n = blockHashes.length\n if (n === 0) return []\n const leavesRequired = isFirstPiece ? 1 << uintBitLength(n - 1) : blocksPerPiece\n const out = blockHashes.slice()\n while (out.length < leavesRequired) {\n out.push(BEP52_ZERO_LEAF.slice())\n }\n return out\n}\n\n/** Root of a “virtual” piece made entirely of zero leaves (BEP 52 file-tree balancing). */\nexport function padPieceRoot(blocksPerPiece: number): Uint8Array {\n const zeros: Uint8Array[] = []\n for (let i = 0; i < blocksPerPiece; i++) {\n zeros.push(BEP52_ZERO_LEAF.slice())\n }\n return rootHashLayer(zeros)\n}\n\nexport interface FileV2MerkleResult {\n /** Total file length in bytes */\n length: number\n /** Merkle root over piece-level hashes (`pieces root` in file tree) */\n piecesRoot?: Uint8Array\n /**\n * Concatenated 32-byte piece subtree roots for `piece layers` dict value.\n * Omitted when file fits in one piece (no `piece layers` entry per BEP 52).\n */\n pieceLayerConcat?: Uint8Array\n /** Per-piece subtree roots (each is root of that piece’s block merkle tree) */\n pieceSubtreeRoots: Uint8Array[]\n}\n\n/**\n * Build BEP 52 per-file merkle metadata from raw file bytes.\n * Mirrors `FileHasher` in the reference `bep_0052_torrent_creator.py`.\n */\nexport function buildFileV2Merkle(fileData: Uint8Array, pieceLength: number): FileV2MerkleResult {\n if (pieceLength < BEP52_BLOCK_SIZE || (pieceLength & (pieceLength - 1)) !== 0) {\n throw new Error('pieceLength must be a power of two and >= 16 KiB')\n }\n\n const blocksPerPiece = pieceLength / BEP52_BLOCK_SIZE\n const pieceSubtreeRoots: Uint8Array[] = []\n let offset = 0\n const len = fileData.length\n\n while (offset < len) {\n const blockHashes: Uint8Array[] = []\n for (let i = 0; i < blocksPerPiece && offset < len; i++) {\n const end = Math.min(offset + BEP52_BLOCK_SIZE, len)\n const block = fileData.subarray(offset, end)\n if (block.length === 0) break\n offset = end\n blockHashes.push(sha256Data(block))\n }\n\n if (blockHashes.length === 0) break\n\n const padded = padPieceBlockHashes(blockHashes, blocksPerPiece, pieceSubtreeRoots.length === 0)\n pieceSubtreeRoots.push(rootHashLayer(padded))\n }\n\n const length = len\n if (length === 0) {\n return { length: 0, pieceSubtreeRoots: [] }\n }\n\n const layerHashes = pieceSubtreeRoots.slice()\n let pieceLayerConcat: Uint8Array | undefined\n if (pieceSubtreeRoots.length > 1) {\n const flat = new Uint8Array(pieceSubtreeRoots.length * 32)\n let p = 0\n for (const h of pieceSubtreeRoots) {\n flat.set(h, p)\n p += 32\n }\n pieceLayerConcat = flat\n }\n\n const padPiece = padPieceRoot(blocksPerPiece)\n const targetPieces = nextPow2(layerHashes.length)\n while (layerHashes.length < targetPieces) {\n layerHashes.push(padPiece.slice())\n }\n\n const piecesRoot = rootHashLayer(layerHashes)\n\n const out: FileV2MerkleResult = {\n length,\n piecesRoot,\n pieceSubtreeRoots,\n }\n if (length > pieceLength && pieceLayerConcat) {\n out.pieceLayerConcat = pieceLayerConcat\n }\n return out\n}\n\n/**\n * Build all layers from leaf hashes (padded to power of two with zero leaves).\n */\nexport function buildMerkleLayers(leaves: Uint8Array[]): Uint8Array[][] {\n if (leaves.length === 0) {\n throw new Error('buildMerkleLayers: no leaves')\n }\n const n = nextPow2(leaves.length)\n const padded: Uint8Array[] = leaves.slice()\n while (padded.length < n) {\n padded.push(BEP52_ZERO_LEAF.slice())\n }\n const layers: Uint8Array[][] = [padded]\n let layer = padded\n while (layer.length > 1) {\n const next: Uint8Array[] = []\n for (let i = 0; i < layer.length; i += 2) {\n next.push(sha256Concat(layer[i]!, layer[i + 1]!))\n }\n layers.push(next)\n layer = next\n }\n return layers\n}\n\n/**\n * Uncle hashes for one leaf index, from leaf layer up `proofLayers` steps.\n * Returns uncles from bottom to top (closest to root last).\n */\nexport function unclesForLeafIndex(layers: Uint8Array[][], leafIndex: number, proofLayers: number): Uint8Array[] {\n const uncles: Uint8Array[] = []\n let idx = leafIndex\n for (let depth = 0; depth < proofLayers && depth < layers.length - 1; depth++) {\n const row = layers[depth]!\n const sibling = idx ^ 1\n if (sibling >= row.length) break\n uncles.push(row[sibling]!.slice())\n idx >>= 1\n }\n return uncles\n}\n\n/**\n * Verify that `leafHash` at `leafIndex` (in padded leaf layer) reaches `expectedRoot`\n * using `uncles` (sibling hashes bottom-up).\n */\n/**\n * Merkle subtree root for one logical piece’s raw bytes (BEP 52).\n * `isFirstPieceOfFile` matches reference `FileHasher` padding (first piece of each file).\n */\nexport function pieceSubtreeRootFromBytes(\n pieceBytes: Uint8Array,\n pieceLength: number,\n isFirstPieceOfFile: boolean\n): Uint8Array {\n const blocksPerPiece = pieceLength / BEP52_BLOCK_SIZE\n const blockHashes: Uint8Array[] = []\n let offset = 0\n while (offset < pieceBytes.length) {\n const end = Math.min(offset + BEP52_BLOCK_SIZE, pieceBytes.length)\n blockHashes.push(sha256Data(pieceBytes.subarray(offset, end)))\n offset = end\n }\n if (blockHashes.length === 0) {\n return rootHashLayer([BEP52_ZERO_LEAF.slice()])\n }\n const padded = padPieceBlockHashes(blockHashes, blocksPerPiece, isFirstPieceOfFile)\n return rootHashLayer(padded)\n}\n\nexport function verifyLeafToRoot(\n leafHash: Uint8Array,\n leafIndex: number,\n uncles: Uint8Array[],\n expectedRoot: Uint8Array\n): boolean {\n let idx = leafIndex\n let acc = leafHash.slice()\n for (const uncle of uncles) {\n const left = (idx & 1) === 0 ? acc : uncle\n const right = (idx & 1) === 0 ? uncle : acc\n acc = sha256Concat(left, right)\n idx >>= 1\n }\n if (acc.length !== expectedRoot.length) return false\n for (let i = 0; i < acc.length; i++) {\n if (acc[i] !== expectedRoot[i]) return false\n }\n return true\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@z-torrent/merkle-tree",
3
+ "description": "BEP 52 BitTorrent v2 merkle hash trees (SHA-256)",
4
+ "version": "0.0.7",
5
+ "type": "module",
6
+ "author": "Dmitriy Skrylnikov <skrylnikovd@ya.ru>",
7
+ "license": "MIT",
8
+ "engines": {
9
+ "node": ">=18.0.0"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsdown",
24
+ "test": "bun test",
25
+ "typecheck": "tsc --noEmit"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/skrylnikov/z-torrent.git",
30
+ "directory": "packages/merkle-tree"
31
+ },
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "keywords": [
36
+ "bittorrent",
37
+ "bep52",
38
+ "merkle",
39
+ "v2",
40
+ "z-torrent"
41
+ ]
42
+ }