|
1 | | -type CommonPath = { length: number; path: bigint }; |
2 | | - |
3 | | -export function calculateCommonPath(path1: bigint, path2: bigint): CommonPath { |
4 | | - let path = 1n; |
5 | | - let mask = 1n; |
6 | | - let length = 0; |
| 1 | +/** |
| 2 | + * Length of the common big-endian bit prefix shared by keys `a` and `b`, capped at `maxDepth` |
| 3 | + * (depth 0 is the most significant bit of byte 0). Used to find where a new key bifurcates from an |
| 4 | + * existing branch: pass `256` for a leaf (compare the whole key) or the node's depth for an interior |
| 5 | + * branch (its stored region is only meaningful up to that depth). |
| 6 | + */ |
| 7 | +export function commonPrefixLength(a: Uint8Array, b: Uint8Array, maxDepth: number): number { |
| 8 | + const fullBytes = maxDepth >> 3; |
| 9 | + for (let i = 0; i < fullBytes; i++) { |
| 10 | + if (a[i] !== b[i]) { |
| 11 | + return (i << 3) + Math.clz32(a[i] ^ b[i]) - 24; |
| 12 | + } |
| 13 | + } |
7 | 14 |
|
8 | | - while ((path1 & mask) === (path2 & mask) && path < path1 && path < path2) { |
9 | | - mask <<= 1n; |
10 | | - length += 1; |
11 | | - path = mask | ((mask - 1n) & path1); |
| 15 | + const remainderBits = maxDepth & 7; |
| 16 | + if (remainderBits > 0) { |
| 17 | + const diff = (a[fullBytes] ^ b[fullBytes]) & (0xff << (8 - remainderBits)); |
| 18 | + if (diff !== 0) { |
| 19 | + return (fullBytes << 3) + Math.clz32(diff) - 24; |
| 20 | + } |
12 | 21 | } |
13 | 22 |
|
14 | | - return { length, path }; |
| 23 | + return maxDepth; |
15 | 24 | } |
16 | 25 |
|
17 | 26 | /** |
18 | | - * Region committed by an interior node: the `depth`-bit common prefix of all leaves in the node's |
19 | | - * sub-tree. The `i`th lowest bit of path will be the `i mod 8`th lowest bit in the `i div 8`th |
20 | | - * byte of the returned 32-byte array (so the packing is little-endian); the remaining bits of the array |
21 | | - * are set to zero. |
| 27 | + * The key's first `depth` bits, with the remaining bits of the 32-byte array zeroed. |
22 | 28 | */ |
23 | | -export function pathToRegion(path: bigint, depth: number): Uint8Array { |
| 29 | +export function regionFromKey(key: Uint8Array, depth: number): Uint8Array { |
24 | 30 | const region = new Uint8Array(32); |
25 | | - const fullBytes = Math.floor(depth / 8); |
26 | | - const remainderBits = depth % 8; |
27 | | - |
28 | | - let bits = path; |
29 | | - for (let j = 0; j < fullBytes; j++, bits >>= 8n) { |
30 | | - region[j] = Number(bits & 0xffn); |
31 | | - } |
| 31 | + const fullBytes = depth >> 3; |
| 32 | + const remainderBits = depth & 7; |
| 33 | + region.set(key.subarray(0, fullBytes)); |
32 | 34 | if (remainderBits > 0) { |
33 | | - region[fullBytes] = Number(bits & 0xffn) & ((1 << remainderBits) - 1); |
| 35 | + region[fullBytes] = key[fullBytes] & ((0xff << (8 - remainderBits)) & 0xff); |
34 | 36 | } |
35 | 37 | return region; |
36 | 38 | } |
37 | 39 |
|
| 40 | +/** |
| 41 | + * Big-endian bit of `data` at the given depth per the Yellowpaper: depth 0 is the most significant |
| 42 | + * bit of `data[0]` (`data[0] & 0x80`) and depth 255 is the least significant bit of `data[31]`. |
| 43 | + */ |
38 | 44 | export function getBitAtDepth(data: Uint8Array, depth: number): number { |
39 | | - depth = Number(depth); |
40 | | - const byteIndex = Math.floor(depth / 8); |
41 | | - const bitInByte = depth % 8; |
42 | | - return (data[byteIndex] >> bitInByte) & 1; |
| 45 | + if (!Number.isInteger(depth) || depth < 0 || depth >= data.length * 8) { |
| 46 | + throw new Error(`Depth ${depth} is out of bounds for a ${data.length}-byte value.`); |
| 47 | + } |
| 48 | + const byteIndex = depth >> 3; |
| 49 | + const bitInByte = depth & 7; |
| 50 | + return (data[byteIndex] >> (7 - bitInByte)) & 1; |
| 51 | +} |
| 52 | + |
| 53 | +/** |
| 54 | + * Render the first `length` big-endian bits of `data` as a `'0'`/`'1'` string: whole bytes first, |
| 55 | + * then the high bits of the partial byte. |
| 56 | + */ |
| 57 | +export function bitsToString(data: Uint8Array, length: number): string { |
| 58 | + if (!Number.isInteger(length) || length < 0 || length > data.length * 8) { |
| 59 | + throw new Error(`Length ${length} is out of bounds for a ${data.length}-byte value.`); |
| 60 | + } |
| 61 | + const fullBytes = length >> 3; |
| 62 | + const remainderBits = length & 7; |
| 63 | + let bits = ''; |
| 64 | + for (let i = 0; i < fullBytes; i++) { |
| 65 | + bits += data[i].toString(2).padStart(8, '0'); |
| 66 | + } |
| 67 | + if (remainderBits > 0) { |
| 68 | + bits += data[fullBytes].toString(2).padStart(8, '0').slice(0, remainderBits); |
| 69 | + } |
| 70 | + return bits; |
43 | 71 | } |
0 commit comments