-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmerkle.ts
304 lines (286 loc) · 9.26 KB
/
merkle.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
/**
* This module provides hash digests of JSON Merkle trees. It can be used for
* efficient diff of two large trees or updating a deep leaf in a large tree.
* @license LGPL-3.0-or-later
*/
import * as ascii85 from "https://deno.land/[email protected]/encoding/ascii85.ts";
import { Tree } from "./canon.ts";
import { crypto, type DigestAlgorithmType } from "./crypto.ts";
import { fromHex, toHex } from "./hex.ts";
/**
* Represents a JSON Merkle tree. It is equivalent to {@link Tree}, except that
* it can contain {@link MerkleHash}es instead of actual JSON values.
*/
export type MerkleTree<T extends DigestAlgorithmType> =
| Tree
| MerkleTreeArray<T>
| MerkleTreeObject<T>
| MerkleHash<T>;
// deno-lint-ignore no-empty-interface
interface MerkleTreeArray<T extends DigestAlgorithmType>
extends Array<MerkleTree<T>> {
}
interface MerkleTreeObject<T extends DigestAlgorithmType> {
[_: string]: MerkleTree<T>;
}
/**
* Represents a hash of a Merkle tree.
*/
export class MerkleHash<T extends DigestAlgorithmType> {
static readonly #MoveFromInternalBuffer: unknown = {};
readonly #hashBuffer: Uint8Array;
/**
* The hash algorithm used to create this digest.
*/
readonly algorithm: T & DigestAlgorithmType;
/**
* Creates a new {@link MerkleHash} from a {@link Uint8Array}.
* @param algorithm The used hash algorithm.
* @param hashBuffer A buffer of digest bytes.
*/
constructor(
algorithm: T & DigestAlgorithmType,
hashBuffer: ArrayBuffer | Uint8Array | number[],
private _moveFromInternalBuffer?: unknown,
) {
if (hashBuffer instanceof ArrayBuffer) {
this.#hashBuffer =
_moveFromInternalBuffer === MerkleHash.#MoveFromInternalBuffer
? new Uint8Array(hashBuffer)
: new Uint8Array(new Uint8Array(hashBuffer));
} else if (hashBuffer instanceof Uint8Array || Array.isArray(hashBuffer)) {
this.#hashBuffer = new Uint8Array(hashBuffer);
} else {
throw new TypeError(`Unexpected type: ${typeof hashBuffer}.`);
}
this.algorithm = algorithm;
}
/**
* Derives a {@link MerkleHash} from input data.
* @param algorithm The hash algorithm to use.
* @param data Input bytes to digest.
* @returns The derived {@link MerkleHash}.
*/
static async deriveFrom<T extends DigestAlgorithmType>(
algorithm: T & DigestAlgorithmType,
data: BufferSource | AsyncIterable<BufferSource> | Iterable<BufferSource>,
): Promise<MerkleHash<T>> {
const digest = await crypto.subtle.digest(algorithm, data);
return new MerkleHash(
algorithm,
digest,
MerkleHash.#MoveFromInternalBuffer,
);
}
/**
* Converts a hexadecimal string into a {@link MerkleHash}.
* @param algorithm The used hash algorithm.
* @param hex The hexadecimal representation of a hash.
* @returns A {@link MerkleHash} object.
*/
static fromHex<T extends DigestAlgorithmType>(
algorithm: T & DigestAlgorithmType,
hex: string,
): MerkleHash<T> {
const bytes = fromHex(hex);
return new MerkleHash(
algorithm,
bytes.buffer,
MerkleHash.#MoveFromInternalBuffer,
);
}
/**
* Shows the hexadecimal representation of the hash.
*/
get hex(): string {
return toHex(this.#hashBuffer);
}
/**
* Shows the base85 (RFC 1924) representation of the hash.
*/
get base85(): string {
return ascii85.encode(this.#hashBuffer, {
"standard": "RFC 1924",
delimiter: false,
});
}
/**
* Checks if two {@link MerkleHash} objects are equal.
* @param other The other {@link MerkleHash} to compare.
* @returns `true` if the hashes are equal, `false` otherwise.
*/
equals(other: MerkleHash<T>): boolean {
if (!(other instanceof MerkleHash) || this.algorithm !== other.algorithm) {
return false;
}
const thisBuffer = this.#hashBuffer;
const otherBuffer = other.#hashBuffer;
const length = thisBuffer.length;
if (length !== otherBuffer.length) return false;
for (let i = 0; i < length; i++) {
if (thisBuffer.at(i) !== otherBuffer.at(i)) return false;
}
return true;
}
/**
* Compares two {@link MerkleHash} objects. The comparison is done by
* comparing the hashes in byte order, that is, it's lexicographic.
* @param other The other {@link MerkleHash} to compare.
* @returns `-1` if this hash goes before the other hash, `0` if they are
* equal, and `1` if this hash goes after the other hash.
* @throws {TypeError} Thrown if the other hash is a different kind of
* {@link MerkleHash}; for example, different hash
* algorithms.
*/
compareTo(other: MerkleHash<T>): number {
if (!(other instanceof MerkleHash)) {
throw new TypeError("Expected a merkle hash.");
}
const thisBuffer = this.#hashBuffer;
const otherBuffer = other.#hashBuffer;
const length = thisBuffer.length;
if (length !== otherBuffer.length) {
throw new TypeError(
`Cannot compare merkle hashes with different sizes.`,
);
} else if (this.algorithm !== other.algorithm) {
throw new TypeError(
`Cannot compare merkle hashes with different algorithms.`,
);
}
for (let i = 0; i < length; i++) {
const a = thisBuffer.at(i);
const b = otherBuffer.at(i);
if (a !== b) return a! - b!;
}
return 0;
}
/**
* Returns the {@link Uint8Array} representation of the hash.
* @returns A copy of the {@link Uint8Array} representation of the hash.
*/
toUint8Array(): Uint8Array {
return new Uint8Array(this.#hashBuffer);
}
/**
* Shows the algorithm and hexadecimal representation of the hash.
* @returns The algorithm and hexadecimal representation of the hash.
*/
toString(): string {
return `${this.algorithm.toString()} ${this.hex}`;
}
[Symbol.for("Deno.customInspect")](): string {
return `MerkleHash ${this.toString()}`;
}
}
const utf8Encoder = new TextEncoder();
// editorconfig-checker-disable
/**
* Derives a {@link MerkleHash} from input data.
*
* It treats {@link MerkleHash}es in the input tree as like JSON values that
* those hashes represent. For example, the following code prints 5 rows of
* the exactly same hashes:
*
* ```ts
* import { MerkleTree, merkle } from "./merkle.ts";
* const tree = [
* {
* title: "The Catcher in the Rye",
* author: "J.D. Salinger",
* content: "Very long content...",
* },
* {
* title: "The Great Gatsby",
* author: "F. Scott Fitzgerald",
* content: "Very long content...",
* },
* ];
*
* const tree2: MerkleTree<"SHA3-256"> = [
* {
* ...tree[0],
* content: await merkle("SHA3-256", tree[0].content),
* },
* tree[1],
* ];
*
* const tree3: MerkleTree<"SHA3-256"> = [
* await merkle("SHA3-256", tree[0]),
* tree[1],
* ];
*
* const tree4: MerkleTree<"SHA3-256"> = [
* await merkle("SHA3-256", tree[0]),
* await merkle("SHA3-256", tree[1]),
* ];
*
* const merkleRoot = await merkle("SHA3-256", tree);
*
* console.log(await merkle("SHA3-256", tree));
* console.log(await merkle("SHA3-256", tree2));
* console.log(await merkle("SHA3-256", tree3));
* console.log(await merkle("SHA3-256", tree4));
* console.log(await merkle("SHA3-256", merkleRoot));
*
* // prints:
* // MerkleHash SHA3-256 ede1427e0292877c45436591ac28139e0a8a1c23cf5d2036ffad6067bbc8c15c
* // MerkleHash SHA3-256 ede1427e0292877c45436591ac28139e0a8a1c23cf5d2036ffad6067bbc8c15c
* // MerkleHash SHA3-256 ede1427e0292877c45436591ac28139e0a8a1c23cf5d2036ffad6067bbc8c15c
* // MerkleHash SHA3-256 ede1427e0292877c45436591ac28139e0a8a1c23cf5d2036ffad6067bbc8c15c
* // MerkleHash SHA3-256 ede1427e0292877c45436591ac28139e0a8a1c23cf5d2036ffad6067bbc8c15c
* ```
*
* @param algorithm The hash algorithm to use.
* @param tree The input JSON data.
* @returns The merkle root hash of the given JSON tree.
*/
// editorconfig-checker-enable
export async function merkle<T extends DigestAlgorithmType>(
algorithm: T & DigestAlgorithmType,
tree: MerkleTree<T>,
): Promise<MerkleHash<T>> {
if (tree instanceof MerkleHash) return tree;
let canon: string;
if (tree == null || typeof tree !== "object") {
// ES6 JSON partially complies with RFC 8785 (JCS: JSON Canonicalization
// Scheme) except for objects and arrays:
canon = JSON.stringify(tree);
} else if (Array.isArray(tree)) {
const buffer: string[] = [];
buffer.push("[");
let notFirst = false;
for (const item of tree) {
buffer.push(notFirst ? ",'" : "'");
notFirst = true;
const itemHash = await merkle(algorithm, item);
buffer.push(itemHash.base85);
buffer.push("'");
}
buffer.push("]");
canon = buffer.join("");
} else {
// Object keys must be sorted.
const keys = Object.keys(tree);
keys.sort();
const buffer: string[] = [];
buffer.push("{");
let notFirst = false;
for (const key of keys) {
if (notFirst) {
buffer.push(",");
}
notFirst = true;
buffer.push(JSON.stringify(key));
buffer.push(":'");
const valueHash = await merkle(algorithm, tree[key]);
buffer.push(valueHash.base85);
buffer.push("'");
}
buffer.push("}");
canon = buffer.join("");
}
const utf8 = utf8Encoder.encode(canon);
return await MerkleHash.deriveFrom(algorithm, utf8);
}
export { type DigestAlgorithmType };