Skip to content

Commit 5d8941e

Browse files
authored
Add length and at(index: number) getters to MerkleTree (#44)
1 parent ee103ec commit 5d8941e

File tree

6 files changed

+313
-274
lines changed

6 files changed

+313
-274
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Added `SimpleMerkleTree` class that supports `bytes32` leaves with no extra hashing.
66
- Support custom hashing function for computing internal nodes. Available in the core and in `SimpleMerkleTree`.
7+
- Add `length` and `at()` (leaf getter) to `StandardMerkleTree` and `SimpleMerkleTree`.
78

89
## 1.0.6
910

src/merkletree.ts

+10
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export interface MerkleTreeData<T> {
2323

2424
export interface MerkleTree<T> {
2525
root: HexString;
26+
length: number;
27+
at(index: number): T | undefined;
2628
render(): string;
2729
dump(): MerkleTreeData<T>;
2830
entries(): Iterable<[number, T]>;
@@ -85,6 +87,14 @@ export abstract class MerkleTreeImpl<T> implements MerkleTree<T> {
8587
return this.tree[0]!;
8688
}
8789

90+
get length(): number {
91+
return this.values.length;
92+
}
93+
94+
at(index: number): T | undefined {
95+
return this.values.at(index)?.value;
96+
}
97+
8898
abstract dump(): MerkleTreeData<T>;
8999

90100
render() {

src/simple.test.ts

+27-16
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ import { test, testProp, fc } from '@fast-check/ava';
22
import { HashZero as zero } from '@ethersproject/constants';
33
import { keccak256 } from '@ethersproject/keccak256';
44
import { SimpleMerkleTree } from './simple';
5-
import { BytesLike, HexString, concat, compare } from './bytes';
5+
import { BytesLike, HexString, concat, compare, toHex } from './bytes';
6+
import { InvalidArgumentError, InvariantError } from './utils/errors';
7+
8+
fc.configureGlobal({ numRuns: process.env.CI ? 5000 : 100 });
69

710
const reverseNodeHash = (a: BytesLike, b: BytesLike): HexString => keccak256(concat([a, b].sort(compare).reverse()));
811
const otherNodeHash = (a: BytesLike, b: BytesLike): HexString => keccak256(reverseNodeHash(a, b)); // double hash
912

10-
import { toHex } from './bytes';
11-
import { InvalidArgumentError, InvariantError } from './utils/errors';
12-
13-
const leaf = fc.uint8Array({ minLength: 32, maxLength: 32 }).map(toHex);
14-
const leaves = fc.array(leaf, { minLength: 1 });
13+
// Use a mix of uint8array and hexstring to cover the Byteslike space
14+
const leaf = fc
15+
.uint8Array({ minLength: 32, maxLength: 32 })
16+
.chain(l => fc.oneof(fc.constant(l), fc.constant(toHex(l))));
17+
const leaves = fc.array(leaf, { minLength: 1, maxLength: 1000 });
1518
const options = fc.record({
1619
sortLeaves: fc.oneof(fc.constant(undefined), fc.boolean()),
1720
nodeHash: fc.oneof(fc.constant(undefined), fc.constant(reverseNodeHash)),
@@ -20,27 +23,31 @@ const options = fc.record({
2023
const tree = fc
2124
.tuple(leaves, options)
2225
.chain(([leaves, options]) => fc.tuple(fc.constant(SimpleMerkleTree.of(leaves, options)), fc.constant(options)));
23-
const treeAndLeaf = fc.tuple(leaves, options).chain(([leaves, options]) =>
26+
const treeAndLeaf = tree.chain(([tree, options]) =>
2427
fc.tuple(
25-
fc.constant(SimpleMerkleTree.of(leaves, options)),
28+
fc.constant(tree),
2629
fc.constant(options),
27-
fc.nat({ max: leaves.length - 1 }).map(index => ({ value: leaves[index]!, index })),
30+
fc.nat({ max: tree.length - 1 }).map(index => ({ value: tree.at(index)!, index })),
2831
),
2932
);
30-
const treeAndLeaves = fc.tuple(leaves, options).chain(([leaves, options]) =>
33+
const treeAndLeaves = tree.chain(([tree, options]) =>
3134
fc.tuple(
32-
fc.constant(SimpleMerkleTree.of(leaves, options)),
35+
fc.constant(tree),
3336
fc.constant(options),
3437
fc
35-
.uniqueArray(fc.nat({ max: leaves.length - 1 }))
36-
.map(indices => indices.map(index => ({ value: leaves[index]!, index }))),
38+
.uniqueArray(fc.nat({ max: tree.length - 1 }))
39+
.map(indices => indices.map(index => ({ value: tree.at(index)!, index }))),
3740
),
3841
);
3942

40-
fc.configureGlobal({ numRuns: process.env.CI ? 10000 : 100 });
41-
4243
testProp('generates a valid tree', [tree], (t, [tree]) => {
4344
t.notThrows(() => tree.validate());
45+
46+
// check leaves enumeration
47+
for (const [index, value] of tree.entries()) {
48+
t.is(value, tree.at(index)!);
49+
}
50+
t.is(tree.at(tree.length), undefined);
4451
});
4552

4653
testProp(
@@ -118,10 +125,14 @@ testProp('dump and load', [tree], (t, [tree, options]) => {
118125
const recoveredTree = SimpleMerkleTree.load(dump, options.nodeHash);
119126
recoveredTree.validate(); // already done in load
120127

128+
// check dump & reconstructed tree
129+
t.is(dump.format, 'simple-v1');
121130
t.is(dump.hash, options.nodeHash ? 'custom' : undefined);
131+
t.true(dump.values.every(({ value }, index) => value === toHex(tree.at(index)!)));
132+
t.true(dump.values.every(({ value }, index) => value === toHex(recoveredTree.at(index)!)));
122133
t.is(tree.root, recoveredTree.root);
134+
t.is(tree.length, recoveredTree.length);
123135
t.is(tree.render(), recoveredTree.render());
124-
t.deepEqual(tree.entries(), recoveredTree.entries());
125136
t.deepEqual(tree.dump(), recoveredTree.dump());
126137
});
127138

0 commit comments

Comments
 (0)