@sanning/proof 0.19.1 → 0.20.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/README.md +30 -29
- package/dist/agent-record.d.ts +62 -0
- package/dist/agent-record.js +527 -0
- package/dist/agent-schemas.generated.d.ts +15 -0
- package/dist/agent-schemas.generated.js +5759 -0
- package/dist/case-file.d.ts +32 -89
- package/dist/case-file.js +41 -93
- package/dist/case.d.ts +339 -822
- package/dist/case.js +667 -1160
- package/dist/chain.d.ts +19 -47
- package/dist/chain.js +78 -205
- package/dist/claims.d.ts +211 -0
- package/dist/claims.js +1081 -0
- package/dist/cli.d.ts +45 -68
- package/dist/cli.js +618 -1022
- package/dist/compose.js +23 -65
- package/dist/crypto.d.ts +7 -8
- package/dist/crypto.js +60 -147
- package/dist/declaration.d.ts +68 -194
- package/dist/declaration.js +175 -518
- package/dist/environment.d.ts +9 -30
- package/dist/environment.js +17 -66
- package/dist/evidence.d.ts +119 -160
- package/dist/evidence.js +744 -1103
- package/dist/exit-codes.d.ts +11 -27
- package/dist/exit-codes.js +24 -59
- package/dist/file-match.d.ts +33 -79
- package/dist/file-match.js +33 -69
- package/dist/grading.d.ts +98 -135
- package/dist/grading.js +224 -380
- package/dist/handoff.d.ts +62 -160
- package/dist/handoff.js +153 -393
- package/dist/index.d.ts +14 -10
- package/dist/index.js +60 -167
- package/dist/json-schema.d.ts +29 -0
- package/dist/json-schema.js +298 -0
- package/dist/merkle.js +16 -28
- package/dist/message-parts.d.ts +56 -212
- package/dist/message-parts.js +142 -502
- package/dist/side-content.d.ts +11 -22
- package/dist/side-content.js +22 -65
- package/dist/types.d.ts +17 -0
- package/dist/types.js +19 -5
- package/dist/verifier.d.ts +40 -114
- package/dist/verifier.js +196 -535
- package/dist/witness-cache.d.ts +24 -45
- package/dist/witness-cache.js +38 -81
- package/dist/witness-lookup.d.ts +36 -57
- package/dist/witness-lookup.js +89 -139
- package/dist/zip-reader.d.ts +18 -33
- package/dist/zip-reader.js +19 -34
- package/package.json +11 -3
- package/src/agent-record.ts +608 -0
- package/src/agent-schemas.generated.ts +5764 -0
- package/src/case-file.ts +45 -109
- package/src/case.ts +814 -1328
- package/src/chain.ts +85 -224
- package/src/claims.ts +1253 -0
- package/src/cli.ts +714 -1176
- package/src/compose.ts +33 -79
- package/src/crypto.ts +60 -147
- package/src/declaration.ts +219 -656
- package/src/environment.ts +17 -65
- package/src/evidence.ts +958 -1381
- package/src/exit-codes.ts +24 -63
- package/src/file-match.ts +40 -89
- package/src/grading.ts +287 -421
- package/src/handoff.ts +175 -427
- package/src/index.ts +131 -177
- package/src/json-schema.ts +295 -0
- package/src/merkle.ts +16 -28
- package/src/message-parts.ts +155 -533
- package/src/side-content.ts +25 -68
- package/src/types.ts +55 -36
- package/src/verifier.ts +202 -526
- package/src/witness-cache.ts +45 -94
- package/src/witness-lookup.ts +107 -177
- package/src/zip-reader.ts +20 -38
- package/vectors/README.md +52 -34
- package/vectors/agent-record.json +2175 -0
- package/vectors/case-file-index.json +1 -1
- package/vectors/case-split.json +2 -2
- package/vectors/chain-reading.json +3 -3
- package/vectors/file-match.json +285 -148
- package/vectors/jcs-reader.json +6 -6
- package/vectors/pack-grading.json +102 -25
- package/vectors/sandbox-evidence.json +573 -581
- package/vectors/stamp-on-chain.json +18 -18
- package/vectors/witness-cache.json +49 -49
- package/dist/agent-proof.d.ts +0 -28
- package/dist/agent-proof.js +0 -226
- package/dist/run-adapters.d.ts +0 -10
- package/dist/run-adapters.js +0 -35
- package/dist/run.d.ts +0 -209
- package/dist/run.js +0 -1405
- package/src/agent-proof.ts +0 -264
- package/src/run-adapters.ts +0 -44
- package/src/run.ts +0 -1598
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// A JSON Schema (draft 2020-12) validator for the keywords the pinned
|
|
2
|
+
// sanning.agent schemas use, and no others. It reads schemas handed to it as
|
|
3
|
+
// data, resolves `$ref` among them by `$id`, and never fetches anything. A
|
|
4
|
+
// keyword outside the supported set throws rather than being skipped: a later
|
|
5
|
+
// pin that adds one must fail loudly, never validate less than it says.
|
|
6
|
+
//
|
|
7
|
+
// Errors are collected in all-errors mode and returned as the reasons of
|
|
8
|
+
// agent-events.md §10 step 4 and §10.1: `unknown_field` (additionalProperties), `missing_field`
|
|
9
|
+
// (required) and `bad_value` (anything else), each at a JSON Pointer.
|
|
10
|
+
|
|
11
|
+
export type JsonSchema = boolean | { readonly [keyword: string]: unknown };
|
|
12
|
+
|
|
13
|
+
export type SchemaReason = "unknown_field" | "missing_field" | "bad_value";
|
|
14
|
+
|
|
15
|
+
export interface SchemaError {
|
|
16
|
+
readonly reason: SchemaReason;
|
|
17
|
+
readonly at: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Keywords that carry no assertion.
|
|
21
|
+
const ANNOTATIONS = new Set(["$schema", "$id", "$defs", "title", "description"]);
|
|
22
|
+
const ASSERTIONS = new Set([
|
|
23
|
+
"$ref",
|
|
24
|
+
"type",
|
|
25
|
+
"const",
|
|
26
|
+
"enum",
|
|
27
|
+
"not",
|
|
28
|
+
"anyOf",
|
|
29
|
+
"properties",
|
|
30
|
+
"additionalProperties",
|
|
31
|
+
"required",
|
|
32
|
+
"minProperties",
|
|
33
|
+
"items",
|
|
34
|
+
"minItems",
|
|
35
|
+
"maxItems",
|
|
36
|
+
"uniqueItems",
|
|
37
|
+
"pattern",
|
|
38
|
+
"minLength",
|
|
39
|
+
"maxLength",
|
|
40
|
+
"minimum",
|
|
41
|
+
"maximum",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
type SchemaObject = { readonly [keyword: string]: unknown };
|
|
45
|
+
|
|
46
|
+
/** Validate against schemas that reference each other by `$id`. */
|
|
47
|
+
export class SchemaSet {
|
|
48
|
+
private readonly documents = new Map<string, SchemaObject>();
|
|
49
|
+
private readonly patterns = new Map<string, RegExp>();
|
|
50
|
+
|
|
51
|
+
constructor(documents: readonly JsonSchema[]) {
|
|
52
|
+
for (const d of documents) {
|
|
53
|
+
if (typeof d !== "object" || typeof d.$id !== "string") {
|
|
54
|
+
throw new Error("json-schema: every document needs an $id");
|
|
55
|
+
}
|
|
56
|
+
supportedOnly(d);
|
|
57
|
+
this.documents.set(stripFragment(d.$id), d);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Every error `value` has against the document whose `$id` is `id`; empty when it validates. */
|
|
62
|
+
validate(id: string, value: unknown): SchemaError[] {
|
|
63
|
+
const doc = this.documents.get(id);
|
|
64
|
+
if (doc === undefined) throw new Error(`json-schema: no document ${id}`);
|
|
65
|
+
const errors: SchemaError[] = [];
|
|
66
|
+
this.check(doc, id, value, "", errors);
|
|
67
|
+
return errors;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private check(schema: JsonSchema, base: string, value: unknown, at: string, errors: SchemaError[]): void {
|
|
71
|
+
if (schema === true) return;
|
|
72
|
+
if (schema === false) {
|
|
73
|
+
errors.push({ reason: "bad_value", at });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const s = schema;
|
|
77
|
+
const bad = (): void => {
|
|
78
|
+
errors.push({ reason: "bad_value", at });
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (s.$ref !== undefined) {
|
|
82
|
+
const [doc, target] = this.resolve(String(s.$ref), base);
|
|
83
|
+
this.check(target, doc, value, at, errors);
|
|
84
|
+
}
|
|
85
|
+
if (s.type !== undefined) {
|
|
86
|
+
const types = Array.isArray(s.type) ? (s.type as string[]) : [s.type as string];
|
|
87
|
+
if (!types.some((t) => hasType(value, t))) bad();
|
|
88
|
+
}
|
|
89
|
+
if (has(s, "const") && !deepEqual(value, s.const)) bad();
|
|
90
|
+
if (s.enum !== undefined && !(s.enum as unknown[]).some((e) => deepEqual(value, e))) bad();
|
|
91
|
+
if (s.not !== undefined && this.passes(s.not as JsonSchema, base, value)) bad();
|
|
92
|
+
if (s.anyOf !== undefined) {
|
|
93
|
+
const branchErrors: SchemaError[] = [];
|
|
94
|
+
let passed = false;
|
|
95
|
+
for (const branch of s.anyOf as JsonSchema[]) {
|
|
96
|
+
const e: SchemaError[] = [];
|
|
97
|
+
this.check(branch, base, value, at, e);
|
|
98
|
+
if (e.length === 0) {
|
|
99
|
+
passed = true;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
branchErrors.push(...e);
|
|
103
|
+
}
|
|
104
|
+
if (!passed) {
|
|
105
|
+
errors.push(...branchErrors);
|
|
106
|
+
bad();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (typeof value === "string") {
|
|
111
|
+
if (s.pattern !== undefined && !this.pattern(String(s.pattern)).test(value)) bad();
|
|
112
|
+
if (s.minLength !== undefined && codePoints(value) < (s.minLength as number)) bad();
|
|
113
|
+
if (s.maxLength !== undefined && codePoints(value) > (s.maxLength as number)) bad();
|
|
114
|
+
}
|
|
115
|
+
if (typeof value === "number") {
|
|
116
|
+
if (s.minimum !== undefined && value < (s.minimum as number)) bad();
|
|
117
|
+
if (s.maximum !== undefined && value > (s.maximum as number)) bad();
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(value)) {
|
|
120
|
+
if (s.minItems !== undefined && value.length < (s.minItems as number)) bad();
|
|
121
|
+
if (s.maxItems !== undefined && value.length > (s.maxItems as number)) bad();
|
|
122
|
+
if (s.uniqueItems === true && !allUnique(value)) bad();
|
|
123
|
+
if (s.items !== undefined) {
|
|
124
|
+
value.forEach((item, i) => this.check(s.items as JsonSchema, base, item, `${at}/${i}`, errors));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (isObject(value)) {
|
|
128
|
+
const props = (s.properties ?? {}) as Record<string, JsonSchema>;
|
|
129
|
+
if (s.required !== undefined) {
|
|
130
|
+
for (const name of s.required as string[]) {
|
|
131
|
+
if (!has(value, name)) errors.push({ reason: "missing_field", at: `${at}/${escapeToken(name)}` });
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (s.minProperties !== undefined && Object.keys(value).length < (s.minProperties as number)) bad();
|
|
135
|
+
for (const key of Object.keys(value)) {
|
|
136
|
+
const child = `${at}/${escapeToken(key)}`;
|
|
137
|
+
if (has(props, key)) {
|
|
138
|
+
this.check(props[key]!, base, value[key], child, errors);
|
|
139
|
+
} else if (s.additionalProperties === false) {
|
|
140
|
+
errors.push({ reason: "unknown_field", at: child });
|
|
141
|
+
} else if (s.additionalProperties !== undefined) {
|
|
142
|
+
this.check(s.additionalProperties as JsonSchema, base, value[key], child, errors);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private passes(schema: JsonSchema, base: string, value: unknown): boolean {
|
|
149
|
+
const e: SchemaError[] = [];
|
|
150
|
+
this.check(schema, base, value, "", e);
|
|
151
|
+
return e.length === 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private resolve(ref: string, base: string): [string, JsonSchema] {
|
|
155
|
+
const url = new URL(ref, base);
|
|
156
|
+
const doc = stripFragment(url.href);
|
|
157
|
+
let target: unknown = this.documents.get(doc);
|
|
158
|
+
if (target === undefined) throw new Error(`json-schema: no document for $ref ${ref}`);
|
|
159
|
+
const fragment = decodeURIComponent(url.hash.slice(1));
|
|
160
|
+
if (fragment !== "") {
|
|
161
|
+
if (!fragment.startsWith("/")) throw new Error(`json-schema: unsupported $ref ${ref}`);
|
|
162
|
+
for (const token of fragment.slice(1).split("/")) {
|
|
163
|
+
const t = unescapeToken(token);
|
|
164
|
+
if (!isObject(target) || !has(target, t)) throw new Error(`json-schema: unresolved $ref ${ref}`);
|
|
165
|
+
target = target[t];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return [doc, target as JsonSchema];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private pattern(source: string): RegExp {
|
|
172
|
+
let re = this.patterns.get(source);
|
|
173
|
+
if (re === undefined) {
|
|
174
|
+
// ECMA-262 with the `u` flag, as JSON Schema reads `pattern`: `$` is the
|
|
175
|
+
// end of the string, with no exception for a final newline.
|
|
176
|
+
re = new RegExp(source, "u");
|
|
177
|
+
this.patterns.set(source, re);
|
|
178
|
+
}
|
|
179
|
+
return re;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Throw on a keyword this validator does not implement, anywhere in `schema`. */
|
|
184
|
+
function supportedOnly(schema: JsonSchema): void {
|
|
185
|
+
const stack: unknown[] = [schema];
|
|
186
|
+
while (stack.length > 0) {
|
|
187
|
+
const s = stack.pop();
|
|
188
|
+
if (typeof s === "boolean") continue;
|
|
189
|
+
if (!isObject(s)) throw new Error("json-schema: a schema is an object or a boolean");
|
|
190
|
+
for (const [k, v] of Object.entries(s)) {
|
|
191
|
+
if (!ANNOTATIONS.has(k) && !ASSERTIONS.has(k)) throw new Error(`json-schema: unsupported keyword ${k}`);
|
|
192
|
+
if (k === "properties" || k === "$defs") stack.push(...Object.values(v as object));
|
|
193
|
+
else if (k === "anyOf") stack.push(...(v as unknown[]));
|
|
194
|
+
else if (k === "items" || k === "not" || k === "additionalProperties") stack.push(v);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function stripFragment(id: string): string {
|
|
200
|
+
const i = id.indexOf("#");
|
|
201
|
+
return i === -1 ? id : id.slice(0, i);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function hasType(value: unknown, type: string): boolean {
|
|
205
|
+
switch (type) {
|
|
206
|
+
case "null":
|
|
207
|
+
return value === null;
|
|
208
|
+
case "boolean":
|
|
209
|
+
return typeof value === "boolean";
|
|
210
|
+
case "string":
|
|
211
|
+
return typeof value === "string";
|
|
212
|
+
case "number":
|
|
213
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
214
|
+
case "integer":
|
|
215
|
+
return Number.isInteger(value);
|
|
216
|
+
case "array":
|
|
217
|
+
return Array.isArray(value);
|
|
218
|
+
case "object":
|
|
219
|
+
return isObject(value);
|
|
220
|
+
default:
|
|
221
|
+
throw new Error(`json-schema: unsupported type ${type}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** String length as JSON Schema counts it: in code points, not UTF-16 code units. */
|
|
226
|
+
function codePoints(s: string): number {
|
|
227
|
+
let n = 0;
|
|
228
|
+
for (const _ of s) n++;
|
|
229
|
+
return n;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function allUnique(items: readonly unknown[]): boolean {
|
|
233
|
+
for (let i = 0; i < items.length; i++) {
|
|
234
|
+
for (let j = i + 1; j < items.length; j++) {
|
|
235
|
+
if (deepEqual(items[i], items[j])) return false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** JSON equality, without recursion so a deeply nested value cannot overflow the stack. */
|
|
242
|
+
export function deepEqual(a: unknown, b: unknown): boolean {
|
|
243
|
+
const stack: [unknown, unknown][] = [[a, b]];
|
|
244
|
+
while (stack.length > 0) {
|
|
245
|
+
const [x, y] = stack.pop()!;
|
|
246
|
+
if (x === y) continue;
|
|
247
|
+
if (Array.isArray(x)) {
|
|
248
|
+
if (!Array.isArray(y) || x.length !== y.length) return false;
|
|
249
|
+
for (let i = 0; i < x.length; i++) stack.push([x[i], y[i]]);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (isObject(x)) {
|
|
253
|
+
if (!isObject(y)) return false;
|
|
254
|
+
const kx = Object.keys(x);
|
|
255
|
+
if (kx.length !== Object.keys(y).length) return false;
|
|
256
|
+
for (const k of kx) {
|
|
257
|
+
if (!has(y, k)) return false;
|
|
258
|
+
stack.push([x[k], y[k]]);
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function isObject(v: unknown): v is Record<string, unknown> {
|
|
268
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function has(obj: object, key: string): boolean {
|
|
272
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** One JSON Pointer reference token (RFC 6901). */
|
|
276
|
+
export function escapeToken(t: string): string {
|
|
277
|
+
return t.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function unescapeToken(t: string): string {
|
|
281
|
+
return t.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Freeze `value` and every object and array inside it. */
|
|
285
|
+
export function deepFreeze<T>(value: T): T {
|
|
286
|
+
const stack: unknown[] = [value];
|
|
287
|
+
while (stack.length > 0) {
|
|
288
|
+
const v = stack.pop();
|
|
289
|
+
if (v !== null && typeof v === "object" && !Object.isFrozen(v)) {
|
|
290
|
+
Object.freeze(v);
|
|
291
|
+
stack.push(...Object.values(v));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return value;
|
|
295
|
+
}
|
package/src/merkle.ts
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
// RFC 9162 binary Merkle tree
|
|
1
|
+
// RFC 9162 binary Merkle tree: build, audit paths, inclusion verification.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// duplicate-last-leaf variant, which produces different roots for
|
|
9
|
-
// non-power-of-two leaf counts.
|
|
10
|
-
//
|
|
11
|
-
// The empty tree (zero leaves) hashes to SHA-256("") — EMPTY_TREE_ROOT_HEX.
|
|
12
|
-
// Conformance is byte-for-byte against the 7 merkle-tree-* vectors of
|
|
13
|
-
// test-vectors-v1.0 (test/conformance.test.ts).
|
|
3
|
+
// SHA-256 with RFC 9162 §2.1 domain separation: leaves prefixed 0x00, interior
|
|
4
|
+
// nodes 0x01. A tree of n leaves splits into a left subtree of k leaves (the
|
|
5
|
+
// largest power of two < n) and a right subtree of n-k, not the Bitcoin
|
|
6
|
+
// duplicate-last-leaf variant. The empty tree hashes to SHA-256("")
|
|
7
|
+
// (EMPTY_TREE_ROOT_HEX).
|
|
14
8
|
|
|
15
9
|
import { sha256Bytes } from "./crypto.js";
|
|
16
10
|
|
|
@@ -31,9 +25,8 @@ export async function nodeHash(left: Uint8Array, right: Uint8Array): Promise<Uin
|
|
|
31
25
|
return sha256Bytes(buf);
|
|
32
26
|
}
|
|
33
27
|
|
|
34
|
-
// The RFC 9162 Merkle Tree Hash of already-hashed leaves
|
|
35
|
-
//
|
|
36
|
-
// yields SHA-256("").
|
|
28
|
+
// The RFC 9162 Merkle Tree Hash of already-hashed leaves (the output of
|
|
29
|
+
// `leafHash`). Zero leaves yields SHA-256("").
|
|
37
30
|
export async function merkleRoot(leafHashes: Uint8Array[]): Promise<Uint8Array> {
|
|
38
31
|
const n = leafHashes.length;
|
|
39
32
|
if (n === 0) return sha256Bytes(new Uint8Array(0));
|
|
@@ -42,9 +35,8 @@ export async function merkleRoot(leafHashes: Uint8Array[]): Promise<Uint8Array>
|
|
|
42
35
|
return nodeHash(await merkleRoot(leafHashes.slice(0, k)), await merkleRoot(leafHashes.slice(k)));
|
|
43
36
|
}
|
|
44
37
|
|
|
45
|
-
// The inclusion proof for the leaf at index m
|
|
46
|
-
//
|
|
47
|
-
// root). Throws RangeError when m is out of range.
|
|
38
|
+
// The inclusion proof for the leaf at index m (RFC 9162 §2.1.3): sibling hashes
|
|
39
|
+
// bottom-up, empty for a single-leaf tree. Throws RangeError when m is out of range.
|
|
48
40
|
export async function auditPath(m: number, leafHashes: Uint8Array[]): Promise<Uint8Array[]> {
|
|
49
41
|
if (!Number.isInteger(m) || m < 0 || m >= leafHashes.length) {
|
|
50
42
|
throw new RangeError("merkle: leaf index out of range");
|
|
@@ -62,10 +54,8 @@ async function path(m: number, leafHashes: Uint8Array[]): Promise<Uint8Array[]>
|
|
|
62
54
|
return [...(await path(m - k, leafHashes.slice(k))), await merkleRoot(leafHashes.slice(0, k))];
|
|
63
55
|
}
|
|
64
56
|
|
|
65
|
-
// Verify an RFC 9162 §2.1.3 inclusion proof. `leaf` is the leaf hash
|
|
66
|
-
//
|
|
67
|
-
// `expectedRoot` is the merkleRoot committed by the checkpoint envelope.
|
|
68
|
-
// True iff the path reconstructs the expected root. Never throws.
|
|
57
|
+
// Verify an RFC 9162 §2.1.3 inclusion proof. `leaf` is the leaf hash, `path` the
|
|
58
|
+
// audit path bottom-up, `expectedRoot` the root the checkpoint commits to. Never throws.
|
|
69
59
|
export async function verifyInclusion(
|
|
70
60
|
leaf: Uint8Array,
|
|
71
61
|
leafIndex: number,
|
|
@@ -78,18 +68,16 @@ export async function verifyInclusion(
|
|
|
78
68
|
}
|
|
79
69
|
if (totalLeaves === 1) return auditPath.length === 0 && bytesEqual(leaf, expectedRoot);
|
|
80
70
|
|
|
81
|
-
// Arithmetic, not bitwise:
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// are exact for every safe integer, and the stated index and count reach
|
|
85
|
-
// this walk as safe integers.
|
|
71
|
+
// Arithmetic, not bitwise: `&` and `>>=` truncate to 32 bits, so a tree past
|
|
72
|
+
// 2**31 leaves would walk a different tree. `% 2` and halving floor are exact
|
|
73
|
+
// for every safe integer.
|
|
86
74
|
let fn = leafIndex;
|
|
87
75
|
let sn = totalLeaves - 1;
|
|
88
76
|
let r = leaf;
|
|
89
77
|
const half = (v: number): number => Math.floor(v / 2);
|
|
90
78
|
|
|
91
79
|
for (const p of auditPath) {
|
|
92
|
-
if (sn === 0) return false; // path longer than the tree depth
|
|
80
|
+
if (sn === 0) return false; // path longer than the tree depth
|
|
93
81
|
if (fn % 2 === 1 || fn === sn) {
|
|
94
82
|
r = await nodeHash(p, r);
|
|
95
83
|
if (fn % 2 === 0) {
|