Skip to content

Commit cba16e4

Browse files
committed
#134 Update SMT based on pull request comments
1 parent a52c20a commit cba16e4

16 files changed

Lines changed: 308 additions & 68 deletions

src/smt/LeafOutOfBoundsError.ts

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

src/smt/SparseMerkleTreePathUtils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ export function regionFromKey(key: Uint8Array, depth: number): Uint8Array {
4242
* bit of `data[0]` (`data[0] & 0x80`) and depth 255 is the least significant bit of `data[31]`.
4343
*/
4444
export function getBitAtDepth(data: Uint8Array, depth: number): number {
45-
depth = Number(depth);
4645
if (!Number.isInteger(depth) || depth < 0 || depth >= data.length * 8) {
4746
throw new Error(`Depth ${depth} is out of bounds for a ${data.length}-byte value.`);
4847
}

src/smt/radix/FinalizedLeafBranch.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,20 @@ 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 } from '../SparseMerkleTreePathUtils.js';
7+
import { bitsToString, commonPrefixLength } from '../SparseMerkleTreePathUtils.js';
88

99
/**
1010
* Finalized leaf in a radix sparse Merkle tree.
1111
*/
1212
export class FinalizedLeafBranch {
13+
public readonly depth: number;
14+
1315
private constructor(
1416
private readonly _key: Uint8Array,
1517
private readonly _data: Uint8Array,
1618
public readonly hash: DataHash,
1719
) {
18-
this._key = new Uint8Array(_key);
19-
this._data = new Uint8Array(_data);
20+
this.depth = _key.length * 8;
2021
}
2122

2223
/**
@@ -66,6 +67,17 @@ export class FinalizedLeafBranch {
6667
return new FinalizedLeafBranch(key, data, hash);
6768
}
6869

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);
79+
}
80+
6981
/**
7082
* @returns {Promise<FinalizedLeafBranch>} This branch (already finalized).
7183
*/
@@ -78,7 +90,7 @@ export class FinalizedLeafBranch {
7890
*/
7991
public toString(): string {
8092
return dedent`
81-
FinalizedLeaf[${bitsToString(this._key, this._key.length * 8)}]
93+
FinalizedLeaf[${bitsToString(this._key, this.depth)}]
8294
Key: ${HexConverter.encode(this._key)}
8395
Data: ${HexConverter.encode(this._data)}
8496
Hash: ${this.hash.toString()}`;

src/smt/radix/FinalizedNodeBranch.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { FinalizedBranch } from './FinalizedBranch.js';
2+
import { PendingBranch } from './PendingBranch.js';
23
import { PendingNodeBranch } from './PendingNodeBranch.js';
34
import { DataHash } from '../../crypto/hash/DataHash.js';
45
import { IDataHasher } from '../../crypto/hash/IDataHasher.js';
56
import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js';
67
import { dedent } from '../../util/StringUtils.js';
7-
import { bitsToString } from '../SparseMerkleTreePathUtils.js';
8+
import { bitsToString, commonPrefixLength } from '../SparseMerkleTreePathUtils.js';
89

910
/**
1011
* Finalized interior node in a radix sparse Merkle tree.
@@ -48,6 +49,16 @@ export class FinalizedNodeBranch {
4849
return new FinalizedNodeBranch(node.path, node.depth, left, right, hash);
4950
}
5051

52+
/**
53+
* Depth at which `key` diverges from this node's committed region, capped at the node's own depth.
54+
*
55+
* @param {Uint8Array} key Key being inserted.
56+
* @returns {number} Common-prefix depth.
57+
*/
58+
public calculateSplitDepth(key: Uint8Array): number {
59+
return commonPrefixLength(key, this._path, this.depth);
60+
}
61+
5162
/**
5263
* @returns {Promise<FinalizedNodeBranch>} This branch (already finalized).
5364
*/
@@ -66,4 +77,24 @@ export class FinalizedNodeBranch {
6677
Left: ${this.left.toString()}
6778
Right: ${this.right.toString()}`;
6879
}
80+
81+
/**
82+
* Derive a pending node with `left` as its left child, reusing this node's committed region.
83+
*
84+
* @param {PendingBranch} left Replacement left child.
85+
* @returns {PendingNodeBranch} New pending node.
86+
*/
87+
public withLeftBranch(left: PendingBranch): PendingNodeBranch {
88+
return PendingNodeBranch.create(this._path, this.depth, left, this.right);
89+
}
90+
91+
/**
92+
* Derive a pending node with `right` as its right child, reusing this node's committed region.
93+
*
94+
* @param {PendingBranch} right Replacement right child.
95+
* @returns {PendingNodeBranch} New pending node.
96+
*/
97+
public withRightBranch(right: PendingBranch): PendingNodeBranch {
98+
return PendingNodeBranch.create(this._path, this.depth, this.left, right);
99+
}
69100
}

src/smt/radix/PendingLeafBranch.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,19 @@ import { IDataHasher } from '../../crypto/hash/IDataHasher.js';
33
import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js';
44
import { HexConverter } from '../../util/HexConverter.js';
55
import { dedent } from '../../util/StringUtils.js';
6-
import { bitsToString } from '../SparseMerkleTreePathUtils.js';
6+
import { bitsToString, commonPrefixLength } from '../SparseMerkleTreePathUtils.js';
77

88
/**
99
* Pending leaf in a radix sparse Merkle tree, awaiting hashing.
1010
*/
1111
export class PendingLeafBranch {
12+
public readonly depth: number;
13+
1214
public constructor(
1315
private readonly _key: Uint8Array,
1416
private readonly _data: Uint8Array,
1517
) {
16-
this._key = new Uint8Array(_key);
17-
this._data = new Uint8Array(_data);
18+
this.depth = _key.length * 8;
1819
}
1920

2021
/**
@@ -40,6 +41,17 @@ export class PendingLeafBranch {
4041
return this._key.slice();
4142
}
4243

44+
/**
45+
* Depth at which `key` diverges from this leaf's key, capped at the leaf's own depth (a full match
46+
* returns `depth`, signalling a duplicate key).
47+
*
48+
* @param {Uint8Array} key Key being inserted.
49+
* @returns {number} Common-prefix depth.
50+
*/
51+
public calculateSplitDepth(key: Uint8Array): number {
52+
return commonPrefixLength(key, this._key, this.depth);
53+
}
54+
4355
/**
4456
* Hash this leaf to produce a finalized branch.
4557
*
@@ -55,7 +67,7 @@ export class PendingLeafBranch {
5567
*/
5668
public toString(): string {
5769
return dedent`
58-
PendingLeaf[${bitsToString(this._key, this._key.length * 8)}]
70+
PendingLeaf[${bitsToString(this._key, this.depth)}]
5971
Key: ${HexConverter.encode(this._key)}
6072
Data: ${HexConverter.encode(this._data)}`;
6173
}

src/smt/radix/PendingNodeBranch.ts

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,18 @@ import { PendingBranch } from './PendingBranch.js';
33
import { IDataHasher } from '../../crypto/hash/IDataHasher.js';
44
import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js';
55
import { dedent } from '../../util/StringUtils.js';
6-
import { bitsToString } from '../SparseMerkleTreePathUtils.js';
6+
import { bitsToString, commonPrefixLength } from '../SparseMerkleTreePathUtils.js';
77

88
/**
99
* Pending interior node in a radix sparse Merkle tree, awaiting hashing.
1010
*/
1111
export class PendingNodeBranch {
12-
public constructor(
12+
private constructor(
1313
private readonly _path: Uint8Array,
1414
public readonly depth: number,
1515
public readonly left: PendingBranch,
1616
public readonly right: PendingBranch,
17-
) {
18-
this._path = new Uint8Array(_path);
19-
}
17+
) {}
2018

2119
/**
2220
* @returns {Uint8Array} Copy of the node's committed region (its `depth`-bit prefix, suffix zeroed).
@@ -25,6 +23,29 @@ export class PendingNodeBranch {
2523
return this._path.slice();
2624
}
2725

26+
/**
27+
* Create a pending node, defensively copying the committed region.
28+
*
29+
* @param {Uint8Array} path Committed region (its `depth`-bit prefix, suffix zeroed).
30+
* @param {number} depth Node depth.
31+
* @param {PendingBranch} left Left child.
32+
* @param {PendingBranch} right Right child.
33+
* @returns {PendingNodeBranch} New pending node.
34+
*/
35+
public static create(path: Uint8Array, depth: number, left: PendingBranch, right: PendingBranch): PendingNodeBranch {
36+
return new PendingNodeBranch(new Uint8Array(path), depth, left, right);
37+
}
38+
39+
/**
40+
* Depth at which `key` diverges from this node's committed region, capped at the node's own depth.
41+
*
42+
* @param {Uint8Array} key Key being inserted.
43+
* @returns {number} Common-prefix depth.
44+
*/
45+
public calculateSplitDepth(key: Uint8Array): number {
46+
return commonPrefixLength(key, this._path, this.depth);
47+
}
48+
2849
/**
2950
* Hash this node (after finalizing children).
3051
*
@@ -44,4 +65,24 @@ export class PendingNodeBranch {
4465
Left: ${this.left.toString()}
4566
Right: ${this.right.toString()}`;
4667
}
68+
69+
/**
70+
* Derive a node with `left` as its left child, reusing this node's committed region without copying it.
71+
*
72+
* @param {PendingBranch} left Replacement left child.
73+
* @returns {PendingNodeBranch} New pending node.
74+
*/
75+
public withLeftBranch(left: PendingBranch): PendingNodeBranch {
76+
return new PendingNodeBranch(this._path, this.depth, left, this.right);
77+
}
78+
79+
/**
80+
* Derive a node with `right` as its right child, reusing this node's committed region without copying it.
81+
*
82+
* @param {PendingBranch} right Replacement right child.
83+
* @returns {PendingNodeBranch} New pending node.
84+
*/
85+
public withRightBranch(right: PendingBranch): PendingNodeBranch {
86+
return new PendingNodeBranch(this._path, this.depth, this.left, right);
87+
}
4788
}

src/smt/radix/SparseMerkleTree.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { LeafExistsError } from '../LeafExistsError.js';
44
import { PendingBranch } from './PendingBranch.js';
55
import { PendingLeafBranch } from './PendingLeafBranch.js';
66
import { PendingNodeBranch } from './PendingNodeBranch.js';
7-
import { commonPrefixLength, getBitAtDepth, regionFromKey } from '../SparseMerkleTreePathUtils.js';
7+
import { getBitAtDepth, regionFromKey } from '../SparseMerkleTreePathUtils.js';
88
import { SparseMerkleTreeRootNode } from './SparseMerkleTreeRootNode.js';
99
import { IDataHasher } from '../../crypto/hash/IDataHasher.js';
1010
import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js';
@@ -73,29 +73,29 @@ export class SparseMerkleTree {
7373

7474
private buildTree(branch: PendingBranch, key: Uint8Array, data: Uint8Array): PendingBranch {
7575
if (branch instanceof PendingLeafBranch || branch instanceof FinalizedLeafBranch) {
76-
const depth = commonPrefixLength(key, branch.path, 256);
77-
if (depth === 256) {
76+
const depth = branch.calculateSplitDepth(key);
77+
if (depth === branch.depth) {
7878
throw new LeafExistsError();
7979
}
8080

8181
// If a leaf must be split from the middle
8282
const isRight = getBitAtDepth(key, depth);
8383
const newBranch = new PendingLeafBranch(key, data);
84-
return new PendingNodeBranch(
84+
return PendingNodeBranch.create(
8585
regionFromKey(key, depth),
8686
depth,
8787
isRight ? branch : newBranch,
8888
isRight ? newBranch : branch,
8989
);
9090
}
9191

92-
const depth = commonPrefixLength(key, branch.path, branch.depth);
92+
const depth = branch.calculateSplitDepth(key);
9393
const isRight = getBitAtDepth(key, depth);
9494

9595
// If node branch is split in the middle
9696
if (depth < branch.depth) {
9797
const newBranch = new PendingLeafBranch(key, data);
98-
return new PendingNodeBranch(
98+
return PendingNodeBranch.create(
9999
regionFromKey(key, depth),
100100
depth,
101101
isRight ? branch : newBranch,
@@ -105,9 +105,9 @@ export class SparseMerkleTree {
105105

106106
// Key belongs inside current node
107107
if (isRight) {
108-
return new PendingNodeBranch(branch.path, branch.depth, branch.left, this.buildTree(branch.right, key, data));
108+
return branch.withRightBranch(this.buildTree(branch.right, key, data));
109109
}
110110

111-
return new PendingNodeBranch(branch.path, branch.depth, this.buildTree(branch.left, key, data), branch.right);
111+
return branch.withLeftBranch(this.buildTree(branch.left, key, data));
112112
}
113113
}

src/smt/radix/SparseMerkleTreeRootNode.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class SparseMerkleTreeRootNode {
4141
}
4242

4343
if (left != null && right != null) {
44-
const node = await new PendingNodeBranch(new Uint8Array(32), 0, left, right).finalize(factory);
44+
const node = await PendingNodeBranch.create(new Uint8Array(32), 0, left, right).finalize(factory);
4545
return new SparseMerkleTreeRootNode(node.left, node.right, node.hash);
4646
}
4747

src/smt/radixsum/FinalizedLeafBranch.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,23 @@ import { IDataHasherFactory } from '../../crypto/hash/IDataHasherFactory.js';
55
import { BigintConverter } from '../../util/BigintConverter.js';
66
import { HexConverter } from '../../util/HexConverter.js';
77
import { dedent } from '../../util/StringUtils.js';
8-
import { bitsToString } from '../SparseMerkleTreePathUtils.js';
8+
import { bitsToString, commonPrefixLength } from '../SparseMerkleTreePathUtils.js';
99

1010
/**
1111
* Finalized leaf in a radix sparse Merkle sum tree. The leaf hash is
1212
* `SHA-256(0x10 || key || data || u256(value))`, where `u256` is the 32-byte
1313
* big-endian encoding of the leaf amount.
1414
*/
1515
export class FinalizedLeafBranch {
16+
public readonly depth: number;
17+
1618
private constructor(
1719
private readonly _key: Uint8Array,
1820
private readonly _data: Uint8Array,
1921
public readonly value: bigint,
2022
public readonly hash: DataHash,
2123
) {
22-
this._key = new Uint8Array(_key);
23-
this._data = new Uint8Array(_data);
24+
this.depth = _key.length * 8;
2425
}
2526

2627
/**
@@ -73,6 +74,17 @@ export class FinalizedLeafBranch {
7374
return new FinalizedLeafBranch(key, data, leaf.value, hash);
7475
}
7576

77+
/**
78+
* Depth at which `key` diverges from this leaf's key, capped at the leaf's own depth (a full match
79+
* returns `depth`, signalling a duplicate key).
80+
*
81+
* @param {Uint8Array} key Key being inserted.
82+
* @returns {number} Common-prefix depth.
83+
*/
84+
public calculateSplitDepth(key: Uint8Array): number {
85+
return commonPrefixLength(key, this._key, this.depth);
86+
}
87+
7688
/**
7789
* @returns {Promise<FinalizedLeafBranch>} This branch (already finalized).
7890
*/
@@ -85,7 +97,7 @@ export class FinalizedLeafBranch {
8597
*/
8698
public toString(): string {
8799
return dedent`
88-
FinalizedLeaf[${bitsToString(this._key, this._key.length * 8)}]
100+
FinalizedLeaf[${bitsToString(this._key, this.depth)}]
89101
Key: ${HexConverter.encode(this._key)}
90102
Data: ${HexConverter.encode(this._data)}
91103
Value: ${this.value}

0 commit comments

Comments
 (0)