Skip to content

Commit 7cdeed7

Browse files
authored
Merge pull request #135 from unicitynetwork/issue-134
#134 Use big endian bit ordering and byte array paths in smt
2 parents 5bfbb67 + cba16e4 commit 7cdeed7

25 files changed

Lines changed: 604 additions & 234 deletions

src/api/InclusionCertificate.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { HashAlgorithm } from '../crypto/hash/HashAlgorithm.js';
55
import { FinalizedBranch } from '../smt/radix/FinalizedBranch.js';
66
import { FinalizedLeafBranch } from '../smt/radix/FinalizedLeafBranch.js';
77
import { SparseMerkleTreeRootNode } from '../smt/radix/SparseMerkleTreeRootNode.js';
8-
import { pathToRegion } from '../smt/SparseMerkleTreePathUtils.js';
9-
import { BitString } from '../util/BitString.js';
8+
import { getBitAtDepth, regionFromKey } from '../smt/SparseMerkleTreePathUtils.js';
109
import { HexConverter } from '../util/HexConverter.js';
1110
import { dedent } from '../util/StringUtils.js';
1211
import { areUint8ArraysEqual } from '../util/TypedArrayUtils.js';
@@ -36,7 +35,6 @@ export class InclusionCertificate {
3635

3736
const siblings: DataHash[] = [];
3837
const bitmap = new Uint8Array(InclusionCertificate.BITMAP_SIZE);
39-
const keyPath = BitString.fromBytesReversedLSB(key).toBigInt();
4038

4139
while (node != null) {
4240
if (node instanceof FinalizedLeafBranch) {
@@ -47,12 +45,12 @@ export class InclusionCertificate {
4745
return new InclusionCertificate(bitmap, siblings);
4846
}
4947

50-
const isRight: bigint = (keyPath >> BigInt(node.depth)) & 1n;
48+
const isRight = getBitAtDepth(key, node.depth);
5149

5250
const sibling = isRight ? node.left : node.right;
5351

5452
if (sibling != null) {
55-
bitmap[Math.floor(node.depth / 8)] |= 1 << (node.depth % 8);
53+
bitmap[node.depth >> 3] |= 0x80 >> (node.depth & 7);
5654
siblings.push(sibling.hash);
5755
}
5856

@@ -121,12 +119,9 @@ export class InclusionCertificate {
121119
.update(value)
122120
.digest();
123121

124-
const keyPath = BitString.fromBytesReversedLSB(key).toBigInt();
125-
const bitmapPath = BitString.fromBytesReversedLSB(this.bitmap).toBigInt();
126-
127122
let position = this.siblings.length;
128123
for (let depth = InclusionCertificate.MAX_DEPTH; depth >= 0; depth--) {
129-
if (!((bitmapPath >> BigInt(depth)) & 1n)) continue;
124+
if (!getBitAtDepth(this.bitmap, depth)) continue;
130125

131126
position -= 1;
132127
if (position < 0) {
@@ -136,7 +131,7 @@ export class InclusionCertificate {
136131
const sibling = this.siblings[position];
137132

138133
let left: Uint8Array, right: Uint8Array;
139-
if ((keyPath >> BigInt(depth)) & 1n) {
134+
if (getBitAtDepth(key, depth)) {
140135
left = sibling.data;
141136
right = hash.data;
142137
} else {
@@ -146,7 +141,7 @@ export class InclusionCertificate {
146141

147142
hash = await new DataHasher(HashAlgorithm.SHA256)
148143
.update(new Uint8Array([0x01, depth]))
149-
.update(pathToRegion(keyPath, depth))
144+
.update(regionFromKey(key, depth))
150145
.update(left)
151146
.update(right)
152147
.digest();

src/payment/SplitAllocationProof.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { CborSerializer } from '../serialization/cbor/CborSerializer.js';
77
import { FinalizedBranch } from '../smt/radixsum/FinalizedBranch.js';
88
import { FinalizedLeafBranch } from '../smt/radixsum/FinalizedLeafBranch.js';
99
import { SparseMerkleSumTreeRootNode } from '../smt/radixsum/SparseMerkleSumTreeRootNode.js';
10+
import { getBitAtDepth } from '../smt/SparseMerkleTreePathUtils.js';
1011
import { BigintConverter } from '../util/BigintConverter.js';
11-
import { BitString } from '../util/BitString.js';
1212
import { HexConverter } from '../util/HexConverter.js';
1313
import { dedent } from '../util/StringUtils.js';
1414
import { areUint8ArraysEqual } from '../util/TypedArrayUtils.js';
@@ -54,7 +54,6 @@ export class SplitAllocationProof {
5454
* @throws {Error} If the key is not present in the tree.
5555
*/
5656
public static create(root: SparseMerkleSumTreeRootNode, key: Uint8Array): SplitAllocationProof {
57-
const keyPath = BitString.fromBytesReversedLSB(key).toBigInt();
5857
const siblings: ISibling[] = [];
5958

6059
let node: FinalizedBranch | SparseMerkleSumTreeRootNode | null = root;
@@ -68,7 +67,7 @@ export class SplitAllocationProof {
6867
return new SplitAllocationProof(siblings);
6968
}
7069

71-
const isRight: number = Number((keyPath >> BigInt(node.depth)) & 1n);
70+
const isRight = getBitAtDepth(key, node.depth);
7271
const sibling = isRight ? node.left : node.right;
7372
if (sibling != null) {
7473
siblings.push({ depth: node.depth, hash: sibling.hash, sum: sibling.value });
@@ -148,7 +147,8 @@ export class SplitAllocationProof {
148147
throw new Error('Data must be 32 bytes long.');
149148
}
150149

151-
const keyPath = BitString.fromBytesReversedLSB(key).toBigInt();
150+
key = new Uint8Array(key);
151+
data = new Uint8Array(data);
152152

153153
let hash = await new DataHasher(HashAlgorithm.SHA256)
154154
.update(new Uint8Array([0x10]))
@@ -164,7 +164,7 @@ export class SplitAllocationProof {
164164
throw new Error('Reconstructed sum overflows 256 bits.');
165165
}
166166

167-
const isRight = Number((keyPath >> BigInt(sibling.depth)) & 1n);
167+
const isRight = getBitAtDepth(key, sibling.depth);
168168
const left = isRight ? { hash: sibling.hash, value: sibling.sum } : { hash, value: sum };
169169
const right = isRight ? { hash, value: sum } : { hash: sibling.hash, value: sibling.sum };
170170

src/smt/LeafExistsError.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Thrown when a sparse Merkle tree insertion targets a key that is already
3+
* present in the tree.
4+
*/
5+
export class LeafExistsError extends Error {
6+
public constructor() {
7+
super('Leaf already exists.');
8+
this.name = 'LeafExistsError';
9+
}
10+
}

src/smt/LeafInBranchError.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/smt/LeafOutOfBoundsError.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.
Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,71 @@
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+
}
714

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+
}
1221
}
1322

14-
return { length, path };
23+
return maxDepth;
1524
}
1625

1726
/**
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.
2228
*/
23-
export function pathToRegion(path: bigint, depth: number): Uint8Array {
29+
export function regionFromKey(key: Uint8Array, depth: number): Uint8Array {
2430
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));
3234
if (remainderBits > 0) {
33-
region[fullBytes] = Number(bits & 0xffn) & ((1 << remainderBits) - 1);
35+
region[fullBytes] = key[fullBytes] & ((0xff << (8 - remainderBits)) & 0xff);
3436
}
3537
return region;
3638
}
3739

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+
*/
3844
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;
4371
}

src/smt/radix/FinalizedLeafBranch.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@ import { IDataHasher } from '../../crypto/hash/IDataHasher.js';
44
import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js';
55
import { HexConverter } from '../../util/HexConverter.js';
66
import { dedent } from '../../util/StringUtils.js';
7+
import { bitsToString, commonPrefixLength } from '../SparseMerkleTreePathUtils.js';
78

89
/**
910
* Finalized leaf in a radix sparse Merkle tree.
1011
*/
1112
export class FinalizedLeafBranch {
12-
public constructor(
13-
public readonly path: bigint,
13+
public readonly depth: number;
14+
15+
private constructor(
1416
private readonly _key: Uint8Array,
1517
private readonly _data: Uint8Array,
1618
public readonly hash: DataHash,
17-
) {}
19+
) {
20+
this.depth = _key.length * 8;
21+
}
1822

1923
/**
2024
* @returns {Uint8Array} Copy of the leaf data bytes.
@@ -30,6 +34,15 @@ export class FinalizedLeafBranch {
3034
return this._key.slice();
3135
}
3236

37+
/**
38+
* Routing key: the leaf's own key, read bit-by-bit during tree construction.
39+
*
40+
* @returns {Uint8Array} Copy of the routing key.
41+
*/
42+
public get path(): Uint8Array {
43+
return this._key.slice();
44+
}
45+
3346
/**
3447
* Hash a {@link PendingLeafBranch} into a finalized leaf.
3548
*
@@ -51,7 +64,18 @@ export class FinalizedLeafBranch {
5164
.update(data)
5265
.digest();
5366

54-
return new FinalizedLeafBranch(leaf.path, key, data, hash);
67+
return new FinalizedLeafBranch(key, data, hash);
68+
}
69+
70+
/**
71+
* Depth at which `key` diverges from this leaf's key, capped at the leaf's own depth (a full match
72+
* returns `depth`, signalling a duplicate key).
73+
*
74+
* @param {Uint8Array} key Key being inserted.
75+
* @returns {number} Common-prefix depth.
76+
*/
77+
public calculateSplitDepth(key: Uint8Array): number {
78+
return commonPrefixLength(key, this._key, this.depth);
5579
}
5680

5781
/**
@@ -66,7 +90,7 @@ export class FinalizedLeafBranch {
6690
*/
6791
public toString(): string {
6892
return dedent`
69-
FinalizedLeaf[${this.path.toString(2)}]
93+
FinalizedLeaf[${bitsToString(this._key, this.depth)}]
7094
Key: ${HexConverter.encode(this._key)}
7195
Data: ${HexConverter.encode(this._data)}
7296
Hash: ${this.hash.toString()}`;

0 commit comments

Comments
 (0)