provared 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1062 -0
- package/LICENSE +202 -0
- package/LICENSE-CC-BY-4.0.txt +396 -0
- package/README.md +689 -0
- package/bin/provared-check.mjs +565 -0
- package/bin/provared-headers.mjs +110 -0
- package/docs/threat-model.md +997 -0
- package/net/headers.js +403 -0
- package/package.json +57 -0
- package/page/check-page.js +162 -0
- package/page/index.html +47 -0
- package/page/render.js +530 -0
- package/page/style.css +150 -0
- package/samples/office-supplies.expected.json +82 -0
- package/samples/office-supplies.jsonl +10 -0
- package/samples/office-supplies.show.json +59 -0
- package/spec/provared-format.md +1672 -0
- package/src/actions.js +90 -0
- package/src/approval.js +73 -0
- package/src/blockstamp.js +198 -0
- package/src/book.js +2232 -0
- package/src/check.d.ts +21 -0
- package/src/check.js +15 -0
- package/src/compare.js +276 -0
- package/src/cover.js +198 -0
- package/src/der.js +192 -0
- package/src/encoding.js +286 -0
- package/src/fields.js +94 -0
- package/src/guard.js +171 -0
- package/src/headers.js +380 -0
- package/src/index.d.ts +2 -0
- package/src/index.js +37 -0
- package/src/jws.js +235 -0
- package/src/keys.js +149 -0
- package/src/pass.js +67 -0
- package/src/passkey-browser.d.ts +20 -0
- package/src/passkey-browser.js +171 -0
- package/src/recorder.js +1115 -0
- package/src/seal.js +133 -0
- package/src/service.js +124 -0
- package/src/signatures.js +147 -0
- package/src/slip.js +401 -0
- package/src/standing.js +181 -0
- package/src/stub.js +140 -0
- package/src/timestamp.js +351 -0
- package/src/tools.js +192 -0
- package/src/tree.js +143 -0
- package/src/types.d.ts +628 -0
- package/src/webauthn.js +119 -0
package/src/tools.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// A connector between an agent's tools and the stub writer.
|
|
2
|
+
//
|
|
3
|
+
// An agent acts by calling tools: functions that its software offers it.
|
|
4
|
+
// recordTools puts each tool behind the stub writer (recorder.js), so that
|
|
5
|
+
// every call asks first, is run only if the slip allows it, and leaves a
|
|
6
|
+
// stub. The agent's software then calls the tools as before.
|
|
7
|
+
//
|
|
8
|
+
// By default the stub of a call names one document, "arguments": the
|
|
9
|
+
// fingerprint of the call's arguments. So the record fixes exactly what was
|
|
10
|
+
// asked for, without holding it: whoever kept the arguments can show them
|
|
11
|
+
// later, and anyone can work out the fingerprint again.
|
|
12
|
+
//
|
|
13
|
+
// It names no agent software and depends on none: a tool is any function.
|
|
14
|
+
|
|
15
|
+
import { Refusal, fingerprint, utf8 } from './encoding.js';
|
|
16
|
+
import * as f from './fields.js';
|
|
17
|
+
|
|
18
|
+
/** How deep the arguments of a call may nest. */
|
|
19
|
+
const MAX_ARGUMENT_DEPTH = 64;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A call through a recorded tool that was not run: the check before acting
|
|
23
|
+
* did not allow it, or the record so far has a problem. "answer" is what
|
|
24
|
+
* the check before acting said (allowed, problems, breaches, needs).
|
|
25
|
+
*/
|
|
26
|
+
export class NotTaken extends Error {
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} tool the name of the tool
|
|
29
|
+
* @param {{allowed: boolean, problems: object[], breaches: object[], needs: string[]}} answer
|
|
30
|
+
*/
|
|
31
|
+
constructor(tool, answer) {
|
|
32
|
+
const first = [...answer.problems, ...answer.breaches][0];
|
|
33
|
+
super(`The tool "${tool}" was not run. ${first ? first.message : 'The check before acting did not allow it.'}`);
|
|
34
|
+
this.name = 'NotTaken';
|
|
35
|
+
this.tool = tool;
|
|
36
|
+
this.answer = answer;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The arguments written in one form, as RFC 8785 sets out: members sorted
|
|
41
|
+
// by name, no spaces, text and numbers as JSON writes them. So the same
|
|
42
|
+
// arguments always give the same fingerprint, whatever order their members
|
|
43
|
+
// were written in.
|
|
44
|
+
function oneForm(value, depth = 0) {
|
|
45
|
+
if (depth > MAX_ARGUMENT_DEPTH) throw new Refusal('bad-field', 'The arguments nest too deeply.');
|
|
46
|
+
if (value === null || typeof value === 'boolean') return String(value);
|
|
47
|
+
if (typeof value === 'string') {
|
|
48
|
+
if (typeof value.isWellFormed === 'function' && !value.isWellFormed()) throw new Refusal('bad-field', 'The arguments hold text that is not well-formed Unicode.');
|
|
49
|
+
return JSON.stringify(value);
|
|
50
|
+
}
|
|
51
|
+
if (typeof value === 'number') {
|
|
52
|
+
if (!Number.isFinite(value)) throw new Refusal('bad-field', 'The arguments hold a number that JSON cannot write.');
|
|
53
|
+
return JSON.stringify(value);
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
// A list is its items, first to last, and nothing else: a gap, or a member with a name, is not JSON.
|
|
57
|
+
if (Object.keys(value).length !== value.length) throw new Refusal('bad-field', 'The arguments hold a list with a gap in it, or with a named member. That is not JSON.');
|
|
58
|
+
const items = [];
|
|
59
|
+
for (let i = 0; i < value.length; i++) items.push(oneForm(value[i], depth + 1));
|
|
60
|
+
return '[' + items.join(',') + ']';
|
|
61
|
+
}
|
|
62
|
+
const proto = typeof value === 'object' ? Object.getPrototypeOf(value) : undefined;
|
|
63
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
64
|
+
throw new Refusal('bad-field', 'The arguments must be JSON: objects, lists, text, numbers, true, false and null.');
|
|
65
|
+
}
|
|
66
|
+
return '{' + Object.keys(value).sort().map((name) => oneForm(name, depth + 1) + ':' + oneForm(value[name], depth + 1)).join(',') + '}';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The fingerprint of the arguments of a call: SHA-256, in base64url, of the
|
|
71
|
+
* arguments written in the one form of RFC 8785. It is what a recorded
|
|
72
|
+
* tool puts into its stub, as the document "arguments".
|
|
73
|
+
* @param {unknown} args JSON: objects, lists, text, numbers, true, false and null
|
|
74
|
+
* @returns {Promise<string>}
|
|
75
|
+
* @throws {Refusal} "bad-field" if the arguments are not JSON
|
|
76
|
+
*/
|
|
77
|
+
export async function argumentsFingerprint(args) {
|
|
78
|
+
return fingerprint(utf8(oneForm(args)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A copy that nothing else holds.
|
|
82
|
+
function copied(value, what) {
|
|
83
|
+
try {
|
|
84
|
+
return structuredClone(value);
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Refusal('bad-field', `${what} must be plain data.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A member of a tool's description that is either a value or a function of
|
|
91
|
+
// the arguments. A function is handed a copy of the arguments of its own:
|
|
92
|
+
// nothing it does to them changes what is recorded, or what the tool is run
|
|
93
|
+
// with. It is called by itself, not as a member of the description: it is
|
|
94
|
+
// not handed the description, and cannot change it.
|
|
95
|
+
const from = (spec, member, args) => {
|
|
96
|
+
const value = spec[member];
|
|
97
|
+
return typeof value === 'function' ? value(structuredClone(args)) : value;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// The slip asks for the person's approval, none was handed over, and nothing else stands in the way.
|
|
101
|
+
const needsOnlyApproval = (answer) => answer.problems.length === 0 && answer.breaches.length > 0 && answer.breaches.every((b) => b.code === 'approval-missing');
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Put an agent's tools behind the stub writer. Each tool is then called as
|
|
105
|
+
* before, with one argument; the call asks first, runs the tool only if the
|
|
106
|
+
* slip allows the action, and writes the stub.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} recorder the stub writer (openRecorder)
|
|
109
|
+
* @param {Record<string, object>} tools by name. For each tool:
|
|
110
|
+
* - "action": the action name the slip uses for what this tool does;
|
|
111
|
+
* - "run": the tool itself, a function of the arguments. It must not call the stub writer;
|
|
112
|
+
* Each function is called by itself, without "this". "amount" and "with" must give their value at once, not a promise.
|
|
113
|
+
* - "with" (optional): the id of the service the tool deals with, as the slip names it, or a function of the arguments that gives it;
|
|
114
|
+
* - "amount" (optional): {unit, value}, or a function of the arguments that gives it;
|
|
115
|
+
* - "details" (optional): a function of the arguments that gives the documents to name in the stub
|
|
116
|
+
* ({name, sha256} each), or false for none. By default: one document, "arguments", the fingerprint
|
|
117
|
+
* of the arguments (argumentsFingerprint);
|
|
118
|
+
* - "countersign" (optional): asks the other side to countersign the stub; a function of the stub and the arguments;
|
|
119
|
+
* - "approve" (optional): asks the person for their approval where the slip asks for one; a function of the
|
|
120
|
+
* request ({action, amount?, with?, details?}) and the arguments, that gives {record} or nothing.
|
|
121
|
+
* @param {object} [options]
|
|
122
|
+
* @param {(stub: object, tool: string) => any} [options.onStub] called after each stub is written, for example to keep the book
|
|
123
|
+
* @returns {Record<string, (args?: any) => Promise<any>>} the tools. A call gives what the tool gives.
|
|
124
|
+
* A call that is not allowed throws NotTaken, and the tool is not run. If the tool itself fails,
|
|
125
|
+
* its error is passed on and no stub is written (see "act" of the stub writer). If "onStub" fails,
|
|
126
|
+
* its error is passed on too: the tool has run by then, and its stub is in the book.
|
|
127
|
+
* @throws {Refusal} "bad-field" if a tool is not described as set out above
|
|
128
|
+
*/
|
|
129
|
+
export function recordTools(recorder, tools, options = {}) {
|
|
130
|
+
if (recorder === null || typeof recorder !== 'object' || typeof recorder.act !== 'function') {
|
|
131
|
+
throw new Refusal('bad-field', 'recorder: must be a stub writer.');
|
|
132
|
+
}
|
|
133
|
+
if (tools === null || typeof tools !== 'object' || Array.isArray(tools)) throw new Refusal('bad-field', 'tools: must be an object that holds the tools by name.');
|
|
134
|
+
if (options === null || typeof options !== 'object') options = {};
|
|
135
|
+
// What the tools are put behind is fixed here too.
|
|
136
|
+
const act = recorder.act.bind(recorder);
|
|
137
|
+
const onStub = typeof options.onStub === 'function' ? options.onStub : null;
|
|
138
|
+
const recorded = {};
|
|
139
|
+
for (const name of Object.keys(tools)) {
|
|
140
|
+
const spec = tools[name];
|
|
141
|
+
if (spec === null || typeof spec !== 'object') throw f.fail(`tools.${name}`, 'must be an object.');
|
|
142
|
+
// What a tool is, is fixed here, through and through: a later change to the caller's own objects changes nothing.
|
|
143
|
+
// Each member is read once: what is checked is what is fixed.
|
|
144
|
+
const fixed = { action: spec.action, run: spec.run, details: spec.details, countersign: spec.countersign, approve: spec.approve, amount: spec.amount, with: spec.with };
|
|
145
|
+
if (typeof fixed.run !== 'function') throw f.fail(`tools.${name}.run`, 'must be a function.');
|
|
146
|
+
f.actionName(fixed.action, `tools.${name}.action`);
|
|
147
|
+
for (const member of ['countersign', 'approve']) {
|
|
148
|
+
if (fixed[member] !== undefined && typeof fixed[member] !== 'function') throw f.fail(`tools.${name}.${member}`, 'must be a function.');
|
|
149
|
+
}
|
|
150
|
+
if (fixed.details !== undefined && fixed.details !== false && typeof fixed.details !== 'function') throw f.fail(`tools.${name}.details`, 'must be a function, or false.');
|
|
151
|
+
for (const member of ['amount', 'with']) if (typeof fixed[member] !== 'function') fixed[member] = copied(fixed[member], `tools.${name}.${member}`);
|
|
152
|
+
for (const member of ['amount', 'with']) if (fixed[member] !== null && typeof fixed[member] === 'object') Object.freeze(fixed[member]);
|
|
153
|
+
Object.freeze(fixed);
|
|
154
|
+
Object.defineProperty(recorded, name, { enumerable: true, value: (args) => call(act, onStub, name, fixed, args) });
|
|
155
|
+
}
|
|
156
|
+
return recorded;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function call(act, onStub, name, spec, args) {
|
|
160
|
+
// The arguments are copied when the call is made. The fingerprint is made
|
|
161
|
+
// from that copy, and the tool is run with a copy of it. Each of the
|
|
162
|
+
// tool's other functions is handed a copy of its own.
|
|
163
|
+
const given = copied(args, `The arguments of the tool "${name}"`);
|
|
164
|
+
const request = { action: spec.action };
|
|
165
|
+
// What the tool's own functions give is copied too: a function that kept
|
|
166
|
+
// what it gave cannot change it afterwards.
|
|
167
|
+
const amount = copied(from(spec, 'amount', given), `tools.${name}.amount`);
|
|
168
|
+
if (amount !== undefined && amount !== null) request.amount = amount;
|
|
169
|
+
const withWhom = copied(from(spec, 'with', given), `tools.${name}.with`);
|
|
170
|
+
if (withWhom !== undefined && withWhom !== null) request.with = withWhom;
|
|
171
|
+
let details;
|
|
172
|
+
if (spec.details === false) details = [];
|
|
173
|
+
else if (typeof spec.details === 'function') details = (await (0, spec.details)(structuredClone(given))) ?? [];
|
|
174
|
+
else details = given === undefined ? [] : [{ name: 'arguments', sha256: await argumentsFingerprint(given) }];
|
|
175
|
+
if (!Array.isArray(details)) throw f.fail(`tools.${name}.details`, 'must give a list of documents.');
|
|
176
|
+
details = copied(details, `tools.${name}.details`);
|
|
177
|
+
if (details.length > 0) request.details = details;
|
|
178
|
+
|
|
179
|
+
const more = {};
|
|
180
|
+
if (typeof spec.countersign === 'function') more.countersign = (stub) => (0, spec.countersign)(stub, structuredClone(given));
|
|
181
|
+
const perform = () => (0, spec.run)(structuredClone(given));
|
|
182
|
+
let outcome = await act(request, perform, more);
|
|
183
|
+
// Where the slip asks for the person's own approval of this action, and
|
|
184
|
+
// that is the one thing missing, the person is asked, once.
|
|
185
|
+
if (!outcome.done && typeof spec.approve === 'function' && needsOnlyApproval(outcome.answer)) {
|
|
186
|
+
const approval = await (0, spec.approve)(structuredClone(request), structuredClone(given));
|
|
187
|
+
if (approval) outcome = await act(request, perform, { ...more, approval });
|
|
188
|
+
}
|
|
189
|
+
if (!outcome.done) throw new NotTaken(name, outcome.answer);
|
|
190
|
+
if (onStub) await onStub(outcome.stub, name);
|
|
191
|
+
return outcome.result;
|
|
192
|
+
}
|
package/src/tree.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// The tree of fingerprints: a Merkle tree built exactly as RFC 6962
|
|
2
|
+
// (Certificate Transparency), section 2.1, sets out, with SHA-256.
|
|
3
|
+
//
|
|
4
|
+
// leaf = SHA-256(0x00 || data)
|
|
5
|
+
// node = SHA-256(0x01 || left || right)
|
|
6
|
+
// a tree of n leaves splits at the largest power of two smaller than n
|
|
7
|
+
//
|
|
8
|
+
// The proof that one leaf is in the tree is the audit path of section
|
|
9
|
+
// 2.1.1. It is checked with the steps of RFC 9162, section 2.1.3.2.
|
|
10
|
+
|
|
11
|
+
import { concatBytes, equalBytes, sha256 } from './encoding.js';
|
|
12
|
+
|
|
13
|
+
const LEAF = new Uint8Array([0x00]);
|
|
14
|
+
const NODE = new Uint8Array([0x01]);
|
|
15
|
+
|
|
16
|
+
/** @param {Uint8Array} data @returns {Promise<Uint8Array>} */
|
|
17
|
+
export function leafHash(data) {
|
|
18
|
+
return sha256(concatBytes(LEAF, data));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function nodeHash(left, right) {
|
|
22
|
+
return sha256(concatBytes(NODE, left, right));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Every level of the tree, from the leaf hashes up to the root. Pairing
|
|
26
|
+
// neighbours and carrying a last odd node up unchanged gives the same tree
|
|
27
|
+
// as the recursive definition in RFC 6962: the test vectors prove it.
|
|
28
|
+
async function levels(leafHashes) {
|
|
29
|
+
const all = [leafHashes];
|
|
30
|
+
let level = leafHashes;
|
|
31
|
+
while (level.length > 1) {
|
|
32
|
+
const next = [];
|
|
33
|
+
for (let i = 0; i + 1 < level.length; i += 2) next.push(await nodeHash(level[i], level[i + 1]));
|
|
34
|
+
if (level.length % 2 === 1) next.push(level[level.length - 1]);
|
|
35
|
+
all.push(next);
|
|
36
|
+
level = next;
|
|
37
|
+
}
|
|
38
|
+
return all;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The root of the tree over the given leaves: the top fingerprint.
|
|
43
|
+
* @param {Uint8Array[]} leaves the data of each leaf
|
|
44
|
+
* @returns {Promise<Uint8Array>} 32 bytes; for no leaves, the hash of nothing, as RFC 6962 defines
|
|
45
|
+
*/
|
|
46
|
+
export async function treeRoot(leaves) {
|
|
47
|
+
if (leaves.length === 0) return sha256(new Uint8Array(0));
|
|
48
|
+
const hashes = [];
|
|
49
|
+
for (const leaf of leaves) hashes.push(await leafHash(leaf));
|
|
50
|
+
const all = await levels(hashes);
|
|
51
|
+
return all[all.length - 1][0];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Work out the root as the leaves arrive, one at a time, so that the root of
|
|
56
|
+
* the first n leaves is at hand after each. It keeps one fingerprint for
|
|
57
|
+
* each complete part of the tree and folds them together from the right.
|
|
58
|
+
* This is the same tree as treeRoot gives: the tests compare the two at
|
|
59
|
+
* every size.
|
|
60
|
+
*/
|
|
61
|
+
export function treeBuilder(from = []) {
|
|
62
|
+
/** @type {{hash: Uint8Array, size: number}[]} */
|
|
63
|
+
const parts = from.slice();
|
|
64
|
+
return {
|
|
65
|
+
/** A builder that carries on from here by itself: what is added to one is not added to the other. */
|
|
66
|
+
fork() {
|
|
67
|
+
return treeBuilder(parts);
|
|
68
|
+
},
|
|
69
|
+
/** @param {Uint8Array} leaf the data of the next leaf */
|
|
70
|
+
async add(leaf) {
|
|
71
|
+
let node = { hash: await leafHash(leaf), size: 1 };
|
|
72
|
+
while (parts.length && parts[parts.length - 1].size === node.size) {
|
|
73
|
+
const left = parts.pop();
|
|
74
|
+
node = { hash: await nodeHash(left.hash, node.hash), size: left.size * 2 };
|
|
75
|
+
}
|
|
76
|
+
parts.push(node);
|
|
77
|
+
},
|
|
78
|
+
/** @returns {Promise<Uint8Array>} the root of the leaves added so far */
|
|
79
|
+
async root() {
|
|
80
|
+
if (parts.length === 0) return sha256(new Uint8Array(0));
|
|
81
|
+
let hash = parts[parts.length - 1].hash;
|
|
82
|
+
for (let i = parts.length - 2; i >= 0; i--) hash = await nodeHash(parts[i].hash, hash);
|
|
83
|
+
return hash;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The audit path for one leaf: the fingerprints needed to work out the root
|
|
90
|
+
* from that leaf alone.
|
|
91
|
+
* @param {Uint8Array[]} leaves
|
|
92
|
+
* @param {number} index which leaf, counting from 0
|
|
93
|
+
* @returns {Promise<Uint8Array[]>}
|
|
94
|
+
*/
|
|
95
|
+
export async function inclusionPath(leaves, index) {
|
|
96
|
+
if (!Number.isInteger(index) || index < 0 || index >= leaves.length) throw new RangeError('no such leaf');
|
|
97
|
+
const hashes = [];
|
|
98
|
+
for (const leaf of leaves) hashes.push(await leafHash(leaf));
|
|
99
|
+
const all = await levels(hashes);
|
|
100
|
+
const path = [];
|
|
101
|
+
let at = index;
|
|
102
|
+
for (let depth = 0; depth < all.length - 1; depth++) {
|
|
103
|
+
const level = all[depth];
|
|
104
|
+
if (at % 2 === 1) path.push(level[at - 1]);
|
|
105
|
+
else if (at + 1 < level.length) path.push(level[at + 1]);
|
|
106
|
+
// else: a last odd node, carried up with no neighbour at this level.
|
|
107
|
+
at = Math.floor(at / 2);
|
|
108
|
+
}
|
|
109
|
+
return path;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check an audit path (RFC 9162, section 2.1.3.2).
|
|
114
|
+
* @param {number} index which leaf, counting from 0
|
|
115
|
+
* @param {number} size how many leaves the tree has
|
|
116
|
+
* @param {Uint8Array} leaf the data of the leaf
|
|
117
|
+
* @param {Uint8Array[]} path
|
|
118
|
+
* @param {Uint8Array} root
|
|
119
|
+
* @returns {Promise<boolean>}
|
|
120
|
+
*/
|
|
121
|
+
export async function verifyInclusion(index, size, leaf, path, root) {
|
|
122
|
+
if (!Number.isSafeInteger(index) || !Number.isSafeInteger(size) || index < 0 || index >= size) return false;
|
|
123
|
+
let fn = index;
|
|
124
|
+
let sn = size - 1;
|
|
125
|
+
let r = await leafHash(leaf);
|
|
126
|
+
for (const p of path) {
|
|
127
|
+
if (sn === 0) return false;
|
|
128
|
+
if (fn % 2 === 1 || fn === sn) {
|
|
129
|
+
r = await nodeHash(p, r);
|
|
130
|
+
if (fn % 2 === 0) {
|
|
131
|
+
while (fn % 2 === 0 && fn !== 0) {
|
|
132
|
+
fn = Math.floor(fn / 2);
|
|
133
|
+
sn = Math.floor(sn / 2);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
r = await nodeHash(r, p);
|
|
138
|
+
}
|
|
139
|
+
fn = Math.floor(fn / 2);
|
|
140
|
+
sn = Math.floor(sn / 2);
|
|
141
|
+
}
|
|
142
|
+
return sn === 0 && equalBytes(r, root);
|
|
143
|
+
}
|