Skip to content

Commit b1fa66c

Browse files
committed
feat(validation): enhance tree data validation and type safety
- Add validateSerializedTree utility for robust data validation - Improve error handling in fromJSON method - Add comprehensive test suite for utility functions - Fix type safety issues in test assertions - Update version to 1.1.0 This commit improves the reliability and type safety of the library by adding proper validation for serialized tree data and enhancing the test suite.
1 parent 871d3a3 commit b1fa66c

File tree

6 files changed

+389
-22
lines changed

6 files changed

+389
-22
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "merkla",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "A production-ready Merkle tree implementation in JavaScript",
55
"main": "dist/merkla.js",
66
"types": "dist/merkla.d.ts",

src/__tests__/index.test.ts

Lines changed: 182 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,191 @@
11
import { describe, expect, it } from "@jest/globals";
2-
import { MerkleTree } from "../index";
2+
import { MerkleTree, MerkleTreeError, defaultHash } from "../index";
3+
import { Buffer } from "buffer";
34

45
describe("MerkleTree", () => {
5-
it("should create a new Merkle tree", () => {
6-
const data = ["a", "b", "c", "d"];
7-
const tree = new MerkleTree(data);
8-
expect(tree).toBeDefined();
9-
expect(tree.root).toBeDefined();
6+
describe("Constructor", () => {
7+
it("should create a tree with valid data", () => {
8+
const data = ["a", "b", "c", "d"];
9+
const tree = new MerkleTree(data);
10+
expect(tree).toBeDefined();
11+
expect(tree.root).toBeDefined();
12+
expect(tree.leafCount).toBe(4);
13+
expect(tree.depth).toBe(3);
14+
});
15+
16+
it("should throw on empty array", () => {
17+
expect(() => new MerkleTree([])).toThrow(MerkleTreeError);
18+
});
19+
20+
it("should throw on invalid hash function", () => {
21+
expect(() => new MerkleTree(["a"], "not a function" as any)).toThrow(MerkleTreeError);
22+
});
23+
24+
it("should handle single leaf", () => {
25+
const tree = new MerkleTree(["a"]);
26+
expect(tree.leafCount).toBe(1);
27+
expect(tree.depth).toBe(1);
28+
expect(tree.root).toBeDefined();
29+
});
30+
31+
it("should handle odd number of leaves", () => {
32+
const tree = new MerkleTree(["a", "b", "c"]);
33+
expect(tree.leafCount).toBe(3);
34+
expect(tree.depth).toBe(3);
35+
});
36+
37+
it("should handle large number of leaves", () => {
38+
const data = Array.from({ length: 1000 }, (_, i) => `data${i}`);
39+
const tree = new MerkleTree(data);
40+
expect(tree.leafCount).toBe(1000);
41+
expect(tree.depth).toBe(11); // log2(1000) rounded up
42+
});
43+
44+
it("should handle Buffer inputs", () => {
45+
const data = [Buffer.from("a"), Buffer.from("b")];
46+
const tree = new MerkleTree(data);
47+
expect(tree.leafCount).toBe(2);
48+
});
49+
50+
it("should handle mixed string and Buffer inputs", () => {
51+
const data = ["a", Buffer.from("b")];
52+
const tree = new MerkleTree(data);
53+
expect(tree.leafCount).toBe(2);
54+
});
1055
});
1156

12-
it("should generate a valid proof", () => {
13-
const data = ["a", "b", "c", "d"];
14-
const tree = new MerkleTree(data);
15-
const proof = tree.getProof(2); // Proof for 'c'
16-
expect(proof).toBeDefined();
17-
expect(Array.isArray(proof)).toBe(true);
57+
describe("Tree Structure", () => {
58+
it("should have correct tree levels", () => {
59+
const tree = new MerkleTree(["a", "b", "c", "d"]);
60+
expect(tree.tree.length).toBe(3); // 4 leaves -> 2 nodes -> 1 root
61+
expect(tree.tree[0].length).toBe(4); // leaves
62+
expect(tree.tree[1].length).toBe(2); // intermediate
63+
expect(tree.tree[2].length).toBe(1); // root
64+
});
65+
66+
it("should handle duplicate leaves", () => {
67+
const tree = new MerkleTree(["a", "a", "a", "a"]);
68+
const uniqueHashes = new Set(tree.getLeaves().map(h => h.toString("hex")));
69+
expect(uniqueHashes.size).toBe(1);
70+
});
1871
});
1972

20-
it("should verify a valid proof", () => {
21-
const data = ["a", "b", "c", "d"];
22-
const tree = new MerkleTree(data);
23-
const proof = tree.getProof(2);
24-
const leafHash = tree.getLeaf(2);
25-
const isValid = MerkleTree.verifyProof(leafHash, proof, tree.root);
26-
expect(isValid).toBe(true);
73+
describe("Proof Generation", () => {
74+
it("should generate valid proof for first leaf", () => {
75+
const tree = new MerkleTree(["a", "b", "c", "d"]);
76+
const proof = tree.getProof(0);
77+
expect(proof.length).toBe(2);
78+
expect(proof[0].position).toBe("right");
79+
});
80+
81+
it("should generate valid proof for last leaf", () => {
82+
const tree = new MerkleTree(["a", "b", "c", "d"]);
83+
const proof = tree.getProof(3);
84+
expect(proof.length).toBe(2);
85+
expect(proof[0].position).toBe("left");
86+
});
87+
88+
it("should throw on invalid index", () => {
89+
const tree = new MerkleTree(["a", "b", "c", "d"]);
90+
expect(() => tree.getProof(-1)).toThrow(MerkleTreeError);
91+
expect(() => tree.getProof(4)).toThrow(MerkleTreeError);
92+
expect(() => tree.getProof(1.5)).toThrow(MerkleTreeError);
93+
});
94+
95+
it("should generate consistent proofs", () => {
96+
const tree = new MerkleTree(["a", "b", "c", "d"]);
97+
const proof1 = tree.getProof(1);
98+
const proof2 = tree.getProof(1);
99+
expect(proof1).toEqual(proof2);
100+
});
101+
});
102+
103+
describe("Proof Verification", () => {
104+
it("should verify valid proof", () => {
105+
const tree = new MerkleTree(["a", "b", "c", "d"]);
106+
const proof = tree.getProof(2);
107+
const leafHash = tree.getLeaf(2);
108+
expect(MerkleTree.verifyProof(leafHash, proof, tree.root)).toBe(true);
109+
});
110+
111+
it("should reject invalid proof", () => {
112+
const tree = new MerkleTree(["a", "b", "c", "d"]);
113+
const proof = tree.getProof(2);
114+
const wrongLeafHash = tree.getLeaf(1);
115+
expect(MerkleTree.verifyProof(wrongLeafHash, proof, tree.root)).toBe(false);
116+
});
117+
118+
it("should throw on invalid proof format", () => {
119+
const tree = new MerkleTree(["a", "b"]);
120+
expect(() => MerkleTree.verifyProof(
121+
tree.getLeaf(0),
122+
[{ sibling: "invalid" as any, position: "right" }],
123+
tree.root
124+
)).toThrow(MerkleTreeError);
125+
});
126+
});
127+
128+
describe("Serialization", () => {
129+
it("should serialize and deserialize correctly", () => {
130+
const original = new MerkleTree(["a", "b", "c", "d"]);
131+
const serialized = original.toJSON();
132+
const reconstructed = MerkleTree.fromJSON(serialized);
133+
134+
expect(reconstructed.root).toEqual(original.root);
135+
expect(reconstructed.leafCount).toBe(original.leafCount);
136+
expect(reconstructed.depth).toBe(original.depth);
137+
});
138+
139+
it("should throw on invalid JSON", () => {
140+
expect(() => MerkleTree.fromJSON("invalid json")).toThrow(MerkleTreeError);
141+
});
142+
143+
it("should throw on malformed tree data", () => {
144+
const invalidData = JSON.stringify({
145+
leaves: ["invalid"],
146+
tree: [["invalid"]]
147+
});
148+
expect(() => MerkleTree.fromJSON(invalidData)).toThrow(MerkleTreeError);
149+
});
150+
});
151+
152+
describe("Custom Hash Function", () => {
153+
it("should work with custom hash function", () => {
154+
const customHash = (data: Buffer) => Buffer.from("custom" + data.toString());
155+
const tree = new MerkleTree(["a", "b"], customHash);
156+
expect(tree.root).toBeDefined();
157+
});
158+
159+
it("should maintain consistency with custom hash", () => {
160+
const customHash = (data: Buffer) => Buffer.from("custom" + data.toString());
161+
const tree1 = new MerkleTree(["a", "b"], customHash);
162+
const tree2 = new MerkleTree(["a", "b"], customHash);
163+
expect(tree1.root).toEqual(tree2.root);
164+
});
165+
});
166+
167+
describe("Edge Cases", () => {
168+
it("should handle very long input strings", () => {
169+
const longString = "a".repeat(10000);
170+
const tree = new MerkleTree([longString]);
171+
expect(tree.root).toBeDefined();
172+
});
173+
174+
it("should handle special characters", () => {
175+
const data = ["!@#$%^&*()", "你好世界", "🌍"];
176+
const tree = new MerkleTree(data);
177+
expect(tree.root).toBeDefined();
178+
});
179+
180+
it("should handle empty strings", () => {
181+
const tree = new MerkleTree(["", "b"]);
182+
expect(tree.root).toBeDefined();
183+
});
184+
185+
it("should handle repeated patterns", () => {
186+
const data = Array(100).fill("a");
187+
const tree = new MerkleTree(data);
188+
expect(tree.root).toBeDefined();
189+
});
27190
});
28191
});

src/__tests__/utils.test.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { describe, expect, it } from "@jest/globals";
2+
import { validateSerializedTree, toBuffer, createLeafNode, validateProof } from "../utils";
3+
import { defaultHash } from "../index";
4+
import { Buffer } from "buffer";
5+
import { MerkleNode, MerkleProof, SerializedTree } from "../types";
6+
7+
describe("Utility Functions", () => {
8+
describe("validateSerializedTree", () => {
9+
it("should accept valid tree data", () => {
10+
const validData: SerializedTree = {
11+
leaves: ["a1b2c3", "d4e5f6"],
12+
tree: [["a1b2c3", "d4e5f6"], ["abcdef"]]
13+
};
14+
expect(() => validateSerializedTree(validData)).not.toThrow();
15+
});
16+
17+
it("should throw on non-array leaves", () => {
18+
const invalidData = {
19+
leaves: "not an array",
20+
tree: [["a1b2c3"]]
21+
};
22+
expect(() => validateSerializedTree(invalidData as unknown as SerializedTree)).toThrow("Invalid tree structure");
23+
});
24+
25+
it("should throw on non-array tree", () => {
26+
const invalidData = {
27+
leaves: ["a1b2c3"],
28+
tree: "not an array"
29+
};
30+
expect(() => validateSerializedTree(invalidData as unknown as SerializedTree)).toThrow("Invalid tree structure");
31+
});
32+
33+
it("should throw on invalid hex in leaves", () => {
34+
const invalidData: SerializedTree = {
35+
leaves: ["a1b2c3", "not hex"],
36+
tree: [["a1b2c3", "d4e5f6"], ["abcdef"]]
37+
};
38+
expect(() => validateSerializedTree(invalidData)).toThrow("Invalid hex string in leaves array");
39+
});
40+
41+
it("should throw on invalid hex in tree", () => {
42+
const invalidData: SerializedTree = {
43+
leaves: ["a1b2c3", "d4e5f6"],
44+
tree: [["a1b2c3", "d4e5f6"], ["not hex"]]
45+
};
46+
expect(() => validateSerializedTree(invalidData)).toThrow("Invalid hex string in tree array");
47+
});
48+
49+
it("should throw on non-array tree level", () => {
50+
const invalidData = {
51+
leaves: ["a1b2c3", "d4e5f6"],
52+
tree: [["a1b2c3", "d4e5f6"], "not an array"]
53+
};
54+
expect(() => validateSerializedTree(invalidData as unknown as SerializedTree)).toThrow("Invalid hex string in tree array");
55+
});
56+
57+
it("should handle empty arrays", () => {
58+
const emptyData: SerializedTree = {
59+
leaves: [],
60+
tree: [[]]
61+
};
62+
expect(() => validateSerializedTree(emptyData)).not.toThrow();
63+
});
64+
65+
it("should handle malformed tree structure", () => {
66+
const malformedData = {
67+
leaves: ["a1b2c3"],
68+
tree: [["a1b2c3"], ["abcdef"], "not an array"]
69+
};
70+
expect(() => validateSerializedTree(malformedData as unknown as SerializedTree)).toThrow("Invalid hex string in tree array");
71+
});
72+
});
73+
74+
describe("toBuffer", () => {
75+
it("should convert string to Buffer", () => {
76+
const result = toBuffer("test");
77+
expect(Buffer.isBuffer(result)).toBe(true);
78+
expect(result.toString()).toBe("test");
79+
});
80+
81+
it("should return Buffer unchanged", () => {
82+
const buffer = Buffer.from("test");
83+
const result = toBuffer(buffer);
84+
expect(result).toBe(buffer);
85+
});
86+
});
87+
88+
describe("createLeafNode", () => {
89+
it("should create node from string", () => {
90+
const node = createLeafNode("test", defaultHash);
91+
expect(node).toEqual({
92+
hash: expect.any(Buffer)
93+
});
94+
expect(Buffer.isBuffer(node.hash)).toBe(true);
95+
});
96+
97+
it("should create node from Buffer", () => {
98+
const buffer = Buffer.from("test");
99+
const node = createLeafNode(buffer, defaultHash);
100+
expect(node).toEqual({
101+
hash: expect.any(Buffer)
102+
});
103+
expect(Buffer.isBuffer(node.hash)).toBe(true);
104+
});
105+
});
106+
107+
describe("validateProof", () => {
108+
it("should validate correct proof", () => {
109+
const leafHash = defaultHash(Buffer.from("leaf"));
110+
const siblingHash = defaultHash(Buffer.from("sibling"));
111+
const rootHash = defaultHash(Buffer.concat([leafHash, siblingHash]));
112+
113+
const proof: MerkleProof[] = [{
114+
position: "right",
115+
hash: siblingHash
116+
}];
117+
118+
expect(validateProof(proof, leafHash, rootHash, defaultHash)).toBe(true);
119+
});
120+
121+
it("should reject incorrect proof", () => {
122+
const leafHash = defaultHash(Buffer.from("leaf"));
123+
const wrongSiblingHash = defaultHash(Buffer.from("wrong"));
124+
const rootHash = defaultHash(Buffer.from("correct root"));
125+
126+
const proof: MerkleProof[] = [{
127+
position: "right",
128+
hash: wrongSiblingHash
129+
}];
130+
131+
expect(validateProof(proof, leafHash, rootHash, defaultHash)).toBe(false);
132+
});
133+
134+
it("should handle multiple proof steps", () => {
135+
const leafHash = defaultHash(Buffer.from("leaf"));
136+
const sibling1Hash = defaultHash(Buffer.from("sibling1"));
137+
const sibling2Hash = defaultHash(Buffer.from("sibling2"));
138+
139+
// Create a two-level proof
140+
const intermediateHash = defaultHash(Buffer.concat([leafHash, sibling1Hash]));
141+
const rootHash = defaultHash(Buffer.concat([intermediateHash, sibling2Hash]));
142+
143+
const proof: MerkleProof[] = [
144+
{
145+
position: "right",
146+
hash: sibling1Hash
147+
},
148+
{
149+
position: "right",
150+
hash: sibling2Hash
151+
}
152+
];
153+
154+
expect(validateProof(proof, leafHash, rootHash, defaultHash)).toBe(true);
155+
});
156+
});
157+
});

0 commit comments

Comments
 (0)