@agenticstechnologies/verify 0.0.0-stage → 1.0.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/LICENSE +17 -0
- package/README.md +153 -2
- package/bin/verify.mjs +105 -0
- package/package.json +33 -4
- package/src/bundle.mjs +364 -0
- package/src/cleared.d.ts +11 -0
- package/src/cleared.mjs +120 -0
- package/src/evidence.mjs +112 -0
- package/src/index.d.ts +32 -0
- package/src/index.mjs +8 -0
- package/src/merkle.mjs +150 -0
- package/src/record.mjs +440 -0
- package/src/solana.mjs +235 -0
package/src/cleared.mjs
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// Cleared-text leaf check. Shared by the evidence export, the proof bundle,
|
|
2
|
+
// and the server (lib/gateway/proof.ts passes its own leafHash).
|
|
3
|
+
//
|
|
4
|
+
// Retention and deletion null free text and set text_cleared_at. They do not
|
|
5
|
+
// rewrite receipt_hash. The older leaf (hash_version null, 2, or 3) covers
|
|
6
|
+
// `outcome`, so a straight recompute misses after that column is null.
|
|
7
|
+
//
|
|
8
|
+
// When text_cleared_at is set:
|
|
9
|
+
// * Cleared fields are not put back. The stored hash is their commitment.
|
|
10
|
+
// * Older leaves: try the historical outcome labels in memory. A match means
|
|
11
|
+
// every field that was not cleared still agrees with the stored hash.
|
|
12
|
+
// The label that matched is not returned.
|
|
13
|
+
// * Version 4: reason, target, outcome, and error_message were inside the
|
|
14
|
+
// leaf. uncleared_digest, sealed when the text was cleared, is the hash of
|
|
15
|
+
// the leaf with those keys removed. Without that digest the leaf fails.
|
|
16
|
+
// An older outcome that is not in the historical list uses the same kind
|
|
17
|
+
// of digest, with outcome removed. Chain linkage is the caller's check.
|
|
18
|
+
//
|
|
19
|
+
// A receipt with no text_cleared_at still has to recompute exactly.
|
|
20
|
+
|
|
21
|
+
export const LEGACY_OUTCOMES = Object.freeze([
|
|
22
|
+
"success",
|
|
23
|
+
"failed",
|
|
24
|
+
"partial",
|
|
25
|
+
"blocked",
|
|
26
|
+
"escalated",
|
|
27
|
+
"timeout",
|
|
28
|
+
"settled",
|
|
29
|
+
"attempted",
|
|
30
|
+
"circuit_open",
|
|
31
|
+
"fallback",
|
|
32
|
+
"retried",
|
|
33
|
+
"deployed",
|
|
34
|
+
"not_deployed",
|
|
35
|
+
"ok",
|
|
36
|
+
"clean",
|
|
37
|
+
"reversed",
|
|
38
|
+
"pending",
|
|
39
|
+
"human_override",
|
|
40
|
+
"escalation_needed",
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const HEX64 = /^[0-9a-f]{64}$/;
|
|
44
|
+
const V4_CLEARED = ["reason", "target", "outcome", "error_message"];
|
|
45
|
+
|
|
46
|
+
function hex64(value) {
|
|
47
|
+
const text = String(value ?? "").trim().toLowerCase();
|
|
48
|
+
return HEX64.test(text) ? text : "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {Record<string, unknown>} canonical
|
|
53
|
+
* @param {string} storedHash
|
|
54
|
+
* @param {(canonical: Record<string, unknown>) => Promise<string>} leafHash
|
|
55
|
+
* @param {readonly string[]} [extraOutcomes]
|
|
56
|
+
*/
|
|
57
|
+
export async function legacyClearedMatches(canonical, storedHash, leafHash, extraOutcomes) {
|
|
58
|
+
const claimed = hex64(storedHash);
|
|
59
|
+
if (!claimed) return false;
|
|
60
|
+
const base = { ...(canonical || {}) };
|
|
61
|
+
delete base.text_cleared_at;
|
|
62
|
+
delete base.uncleared_digest;
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
const list = extraOutcomes ? [...LEGACY_OUTCOMES, ...extraOutcomes] : LEGACY_OUTCOMES;
|
|
65
|
+
for (const outcome of list) {
|
|
66
|
+
if (typeof outcome !== "string") continue;
|
|
67
|
+
const label = outcome.trim();
|
|
68
|
+
if (!label || seen.has(label)) continue;
|
|
69
|
+
seen.add(label);
|
|
70
|
+
if (await leafHash({ ...base, outcome: label }) === claimed) return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Direct recompute, then the cleared-text rule.
|
|
77
|
+
* `meta.text_cleared_at` is the sibling flag. It is not part of the leaf.
|
|
78
|
+
*
|
|
79
|
+
* @param {Record<string, unknown>} canonical
|
|
80
|
+
* @param {string} storedHash
|
|
81
|
+
* @param {(canonical: Record<string, unknown>) => Promise<string>} leafHash
|
|
82
|
+
* @param {{ text_cleared_at?: unknown, hash_version?: unknown, uncleared_digest?: unknown, status?: unknown }} [meta]
|
|
83
|
+
*/
|
|
84
|
+
export async function clearedLeafMatches(canonical, storedHash, leafHash, meta) {
|
|
85
|
+
const raw = String(storedHash ?? "");
|
|
86
|
+
const claimed = hex64(raw);
|
|
87
|
+
let recomputed = "";
|
|
88
|
+
try {
|
|
89
|
+
recomputed = await leafHash(canonical || {});
|
|
90
|
+
} catch {
|
|
91
|
+
recomputed = "";
|
|
92
|
+
}
|
|
93
|
+
if (recomputed && (recomputed === raw || recomputed === claimed)) return true;
|
|
94
|
+
const cleared = meta?.text_cleared_at || canonical?.text_cleared_at;
|
|
95
|
+
if (!cleared) return false;
|
|
96
|
+
const version = Number(meta?.hash_version ?? canonical?.hash_version);
|
|
97
|
+
if (version === 4) {
|
|
98
|
+
if (!claimed) return false;
|
|
99
|
+
const digest = hex64(meta?.uncleared_digest || canonical?.uncleared_digest);
|
|
100
|
+
if (!digest) return false;
|
|
101
|
+
const body = { ...(canonical || {}) };
|
|
102
|
+
delete body.receipt_id;
|
|
103
|
+
delete body.batch_id;
|
|
104
|
+
delete body.text_cleared_at;
|
|
105
|
+
delete body.uncleared_digest;
|
|
106
|
+
for (const key of V4_CLEARED) delete body[key];
|
|
107
|
+
const partial = await leafHash(body);
|
|
108
|
+
return partial === digest;
|
|
109
|
+
}
|
|
110
|
+
const extra = [];
|
|
111
|
+
if (typeof meta?.status === "string" && meta.status.trim()) extra.push(meta.status.trim());
|
|
112
|
+
if (await legacyClearedMatches(canonical, claimed, leafHash, extra)) return true;
|
|
113
|
+
const digest = hex64(meta?.uncleared_digest || canonical?.uncleared_digest);
|
|
114
|
+
if (!digest) return false;
|
|
115
|
+
const body = { ...(canonical || {}) };
|
|
116
|
+
delete body.text_cleared_at;
|
|
117
|
+
delete body.uncleared_digest;
|
|
118
|
+
delete body.outcome;
|
|
119
|
+
return (await leafHash(body)) === digest;
|
|
120
|
+
}
|
package/src/evidence.mjs
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Leaf and prev_hash checks for GET /api/proof/export JSON.
|
|
2
|
+
// Moved from scripts/verify-evidence.mjs so the package and that script share
|
|
3
|
+
// one implementation. Behavior is unchanged:
|
|
4
|
+
// sha256Hex(JSON.stringify(canonicalize(canonical minus {receipt_id, batch_id})))
|
|
5
|
+
// canonicalize sorts object keys recursively and drops null and undefined.
|
|
6
|
+
// After the first row, prev_hash must equal the previous receipt_hash.
|
|
7
|
+
// The first prev_hash must be 64 hex characters (genesis, or a receipt older
|
|
8
|
+
// than the export window). A truncated export is not a pass.
|
|
9
|
+
// When text_cleared_at is set, the stored hash covers the cleared fields
|
|
10
|
+
// and every field that was not cleared is still checked.
|
|
11
|
+
|
|
12
|
+
import { clearedLeafMatches } from "./cleared.mjs";
|
|
13
|
+
|
|
14
|
+
export function canonicalize(v) {
|
|
15
|
+
if (v === null || v === undefined) return undefined;
|
|
16
|
+
if (Array.isArray(v)) return v.map(canonicalize);
|
|
17
|
+
if (typeof v === "object") {
|
|
18
|
+
const out = {};
|
|
19
|
+
for (const k of Object.keys(v).sort()) {
|
|
20
|
+
const cv = canonicalize(v[k]);
|
|
21
|
+
if (cv !== undefined) out[k] = cv;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
return v;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function sha256Hex(input) {
|
|
29
|
+
const data = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
30
|
+
const buf = await crypto.subtle.digest("SHA-256", data);
|
|
31
|
+
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function leafHash(canonical) {
|
|
35
|
+
const rest = { ...canonical };
|
|
36
|
+
delete rest.receipt_id;
|
|
37
|
+
delete rest.batch_id;
|
|
38
|
+
return sha256Hex(JSON.stringify(canonicalize(rest)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { clearedLeafMatches };
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {unknown} bundle
|
|
45
|
+
* @returns {Promise<{ ok: boolean, checked: number, failures: Array<{ receipt_hash: string, prev_hash: string, hash_ok: boolean, chain_ok: boolean, reason: string }> }>}
|
|
46
|
+
*/
|
|
47
|
+
export async function verifyEvidenceExport(bundle) {
|
|
48
|
+
const receipts = bundle && Array.isArray(bundle.receipts) ? bundle.receipts : null;
|
|
49
|
+
if (!receipts) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
checked: 0,
|
|
53
|
+
failures: [{
|
|
54
|
+
receipt_hash: "",
|
|
55
|
+
prev_hash: "",
|
|
56
|
+
hash_ok: false,
|
|
57
|
+
chain_ok: false,
|
|
58
|
+
reason: "bundle.receipts is missing",
|
|
59
|
+
}],
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const failures = [];
|
|
63
|
+
for (let i = 0; i < receipts.length; i++) {
|
|
64
|
+
const row = receipts[i] ?? {};
|
|
65
|
+
const canonical = row.canonical ?? {};
|
|
66
|
+
const claimed = String(row.receipt_hash ?? "");
|
|
67
|
+
const prev = String(canonical.prev_hash ?? "");
|
|
68
|
+
let recomputed = "";
|
|
69
|
+
try {
|
|
70
|
+
recomputed = await leafHash(canonical);
|
|
71
|
+
} catch {
|
|
72
|
+
recomputed = "";
|
|
73
|
+
}
|
|
74
|
+
const hashOk = recomputed === claimed || await clearedLeafMatches(canonical, claimed, leafHash, {
|
|
75
|
+
text_cleared_at: row.text_cleared_at,
|
|
76
|
+
hash_version: row.hash_version,
|
|
77
|
+
uncleared_digest: row.uncleared_digest,
|
|
78
|
+
status: row.status,
|
|
79
|
+
});
|
|
80
|
+
let chainOk = true;
|
|
81
|
+
let reason = null;
|
|
82
|
+
if (!hashOk) reason = `leaf recomputed ${recomputed} != ${claimed}`;
|
|
83
|
+
if (i === 0) {
|
|
84
|
+
if (!/^[0-9a-f]{64}$/.test(prev)) {
|
|
85
|
+
chainOk = false;
|
|
86
|
+
reason = reason ?? "prev_hash is not 64 hex characters";
|
|
87
|
+
}
|
|
88
|
+
} else if (prev !== String(receipts[i - 1].receipt_hash ?? "")) {
|
|
89
|
+
chainOk = false;
|
|
90
|
+
reason = reason ?? "prev_hash does not match the previous receipt_hash";
|
|
91
|
+
}
|
|
92
|
+
if (!hashOk || !chainOk) {
|
|
93
|
+
failures.push({
|
|
94
|
+
receipt_hash: claimed,
|
|
95
|
+
prev_hash: prev,
|
|
96
|
+
hash_ok: hashOk,
|
|
97
|
+
chain_ok: chainOk,
|
|
98
|
+
reason: reason ?? "failed",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (bundle.verification && bundle.verification.truncated === true) {
|
|
103
|
+
failures.push({
|
|
104
|
+
receipt_hash: "",
|
|
105
|
+
prev_hash: "",
|
|
106
|
+
hash_ok: false,
|
|
107
|
+
chain_ok: false,
|
|
108
|
+
reason: "export was truncated; refuse to call it complete",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return { ok: failures.length === 0, checked: receipts.length, failures };
|
|
112
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function canonicalize(value: unknown): unknown;
|
|
2
|
+
export function sha256Hex(input: string): Promise<string>;
|
|
3
|
+
export function leafHash(canonical: unknown): Promise<string>;
|
|
4
|
+
export function verifyEvidenceExport(bundle: unknown): Promise<unknown>;
|
|
5
|
+
|
|
6
|
+
export function merkleRoot(leaves: string[]): Promise<string | null>;
|
|
7
|
+
export function merkleProof(leaves: string[], index: number): Promise<string[] | null>;
|
|
8
|
+
export function verifyProof(leaf: string, proof: string[] | null | undefined, root: string): Promise<boolean>;
|
|
9
|
+
|
|
10
|
+
/** Tree rule v2 (RFC 9162 2.1). Batches name their rule in tree_rule; absent is v1. */
|
|
11
|
+
export const TREE_RULE_V1: "v1";
|
|
12
|
+
export const TREE_RULE_V2: "v2";
|
|
13
|
+
export function treeRuleOf(value: unknown): "v1" | "v2" | null;
|
|
14
|
+
export function leafHashV2(leaf: string): Promise<string>;
|
|
15
|
+
export function merkleRootV2(leaves: string[]): Promise<string | null>;
|
|
16
|
+
export function merkleProofV2(leaves: string[], index: number): Promise<string[] | null>;
|
|
17
|
+
export function verifyProofV2(leaf: string, index: number, size: number, proof: string[] | null | undefined, root: string): Promise<boolean>;
|
|
18
|
+
|
|
19
|
+
export function verifyBundle(bundle: unknown, opts?: Record<string, unknown>): Promise<unknown>;
|
|
20
|
+
|
|
21
|
+
export function agentRecord(events: unknown, asOf?: string): unknown;
|
|
22
|
+
export function recordBundle(input: { handle: string; as_of?: string; outcomes: unknown }): unknown;
|
|
23
|
+
export function signRecord(privateJwk: Record<string, unknown>, bundle: unknown): Promise<unknown>;
|
|
24
|
+
export function verifyRecord(bundle: unknown, jwks: unknown): Promise<unknown>;
|
|
25
|
+
export function isRecordBundle(bundle: unknown): boolean;
|
|
26
|
+
|
|
27
|
+
export function classifyCluster(
|
|
28
|
+
hint: string | null | undefined,
|
|
29
|
+
explorer?: string | null | undefined,
|
|
30
|
+
): "devnet" | "testnet" | "mainnet" | null;
|
|
31
|
+
export function checkSolanaMemo(opts: Record<string, unknown>): Promise<unknown>;
|
|
32
|
+
export const PUBLIC_RPC: { readonly devnet: string; readonly testnet: string };
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { canonicalize, sha256Hex, leafHash, verifyEvidenceExport } from "./evidence.mjs";
|
|
2
|
+
export {
|
|
3
|
+
merkleRoot, merkleProof, verifyProof,
|
|
4
|
+
TREE_RULE_V1, TREE_RULE_V2, treeRuleOf, leafHashV2, merkleRootV2, merkleProofV2, verifyProofV2,
|
|
5
|
+
} from "./merkle.mjs";
|
|
6
|
+
export { verifyBundle } from "./bundle.mjs";
|
|
7
|
+
export { agentRecord, recordBundle, signRecord, verifyRecord, isRecordBundle } from "./record.mjs";
|
|
8
|
+
export { classifyCluster, checkSolanaMemo, PUBLIC_RPC } from "./solana.mjs";
|
package/src/merkle.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// Sorted-pair Merkle, same fold as netlify/edge-functions/lib/ledger.js
|
|
2
|
+
// (imported by lib/gateway/proof.ts for buildBatch / foldLeafAtIndex):
|
|
3
|
+
// hashPair(a, b) = sha256Hex(a <= b ? a + b : b + a)
|
|
4
|
+
// odd node duplicated; empty leaf list → null (tree rule v1; v2 is at the end of this file)
|
|
5
|
+
// A proof is only the sibling list. Pair order does not depend on index.
|
|
6
|
+
|
|
7
|
+
import { sha256Hex } from "./evidence.mjs";
|
|
8
|
+
|
|
9
|
+
async function hashPair(a, b) {
|
|
10
|
+
return sha256Hex(a <= b ? a + b : b + a);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function merkleRoot(leaves) {
|
|
14
|
+
if (!leaves || !leaves.length) return null;
|
|
15
|
+
let level = leaves.slice();
|
|
16
|
+
while (level.length > 1) {
|
|
17
|
+
const next = [];
|
|
18
|
+
for (let i = 0; i < level.length; i += 2) {
|
|
19
|
+
next.push(await hashPair(level[i], level[i + 1] ?? level[i]));
|
|
20
|
+
}
|
|
21
|
+
level = next;
|
|
22
|
+
}
|
|
23
|
+
return level[0];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function merkleProof(leaves, index) {
|
|
27
|
+
if (index < 0 || index >= leaves.length) return null;
|
|
28
|
+
const proof = [];
|
|
29
|
+
let level = leaves.slice();
|
|
30
|
+
let idx = index;
|
|
31
|
+
while (level.length > 1) {
|
|
32
|
+
const next = [];
|
|
33
|
+
for (let i = 0; i < level.length; i += 2) {
|
|
34
|
+
const left = level[i];
|
|
35
|
+
const right = level[i + 1] ?? level[i];
|
|
36
|
+
if (i === idx) proof.push(right);
|
|
37
|
+
else if (i + 1 === idx) proof.push(left);
|
|
38
|
+
next.push(await hashPair(left, right));
|
|
39
|
+
}
|
|
40
|
+
level = next;
|
|
41
|
+
idx = Math.floor(idx / 2);
|
|
42
|
+
}
|
|
43
|
+
return proof;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function verifyProof(leafHash, proof, root) {
|
|
47
|
+
let h = leafHash;
|
|
48
|
+
for (const sibling of proof || []) h = await hashPair(h, sibling);
|
|
49
|
+
return h === root;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── Tree rule v2 (L3-16): RFC 9162 section 2.1 (RFC 6962), over the 32 bytes of each hex leaf. ──
|
|
53
|
+
// leaf = SHA-256(0x00 || leaf bytes), node = SHA-256(0x01 || left || right), in position order.
|
|
54
|
+
// An odd last node is promoted unchanged (never paired with itself); empty → null; one leaf → its leaf hash.
|
|
55
|
+
// A v2 proof is the RFC audit path; folding it needs the leaf index and the tree size. Batches anchored under v1
|
|
56
|
+
// stay v1 for ever (anchor_batches.tree_rule): a verifier picks the fold by rule and never re-folds an old batch.
|
|
57
|
+
export const TREE_RULE_V1 = "v1";
|
|
58
|
+
export const TREE_RULE_V2 = "v2";
|
|
59
|
+
|
|
60
|
+
function hexBytes(hex) {
|
|
61
|
+
if (typeof hex !== "string" || !/^[0-9a-f]{64}$/.test(hex)) throw new Error("merkle v2: a node must be 64 lowercase hex characters");
|
|
62
|
+
const out = new Uint8Array(32);
|
|
63
|
+
for (let i = 0; i < 32; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function prefixedHash(prefix, ...parts) {
|
|
68
|
+
const buf = new Uint8Array(1 + parts.length * 32);
|
|
69
|
+
buf[0] = prefix;
|
|
70
|
+
parts.forEach((p, i) => buf.set(p, 1 + i * 32));
|
|
71
|
+
return new Uint8Array(await crypto.subtle.digest("SHA-256", buf));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const bytesHex = (b) => [...b].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
75
|
+
|
|
76
|
+
/** v2 leaf hash of a 64-hex leaf: SHA-256(0x00 || bytes), as hex. */
|
|
77
|
+
export async function leafHashV2(leaf) {
|
|
78
|
+
return bytesHex(await prefixedHash(0x00, hexBytes(leaf)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function levelsV2(leaves) {
|
|
82
|
+
let level = [];
|
|
83
|
+
for (const l of leaves) level.push(await prefixedHash(0x00, hexBytes(l)));
|
|
84
|
+
const levels = [level];
|
|
85
|
+
while (level.length > 1) {
|
|
86
|
+
const next = [];
|
|
87
|
+
for (let i = 0; i + 1 < level.length; i += 2) next.push(await prefixedHash(0x01, level[i], level[i + 1]));
|
|
88
|
+
if (level.length % 2 === 1) next.push(level[level.length - 1]);
|
|
89
|
+
level = next;
|
|
90
|
+
levels.push(level);
|
|
91
|
+
}
|
|
92
|
+
return levels;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function merkleRootV2(leaves) {
|
|
96
|
+
if (!leaves || !leaves.length) return null;
|
|
97
|
+
const levels = await levelsV2(leaves);
|
|
98
|
+
return bytesHex(levels[levels.length - 1][0]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The RFC audit path for the leaf at `index`, leaf to root, as hex. */
|
|
102
|
+
export async function merkleProofV2(leaves, index) {
|
|
103
|
+
if (!leaves || !Number.isInteger(index) || index < 0 || index >= leaves.length) return null;
|
|
104
|
+
const levels = await levelsV2(leaves);
|
|
105
|
+
const path = [];
|
|
106
|
+
let idx = index;
|
|
107
|
+
for (const level of levels.slice(0, -1)) {
|
|
108
|
+
const sib = idx ^ 1;
|
|
109
|
+
if (sib < level.length) path.push(bytesHex(level[sib]));
|
|
110
|
+
idx >>= 1;
|
|
111
|
+
}
|
|
112
|
+
return path;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** RFC 9162 2.1.3.2: fold `proof` from leaf `index` of a tree of `size` leaves and compare with `root`. */
|
|
116
|
+
export async function verifyProofV2(leaf, index, size, proof, root) {
|
|
117
|
+
try {
|
|
118
|
+
if (!Number.isInteger(index) || !Number.isInteger(size) || index < 0 || index >= size) return false;
|
|
119
|
+
let fn = index;
|
|
120
|
+
let sn = size - 1;
|
|
121
|
+
let r = await prefixedHash(0x00, hexBytes(leaf));
|
|
122
|
+
for (const hex of proof || []) {
|
|
123
|
+
const p = hexBytes(hex);
|
|
124
|
+
if (sn === 0) return false;
|
|
125
|
+
if ((fn & 1) === 1 || fn === sn) {
|
|
126
|
+
r = await prefixedHash(0x01, p, r);
|
|
127
|
+
if ((fn & 1) === 0) {
|
|
128
|
+
while ((fn & 1) === 0 && fn !== 0) {
|
|
129
|
+
fn >>= 1;
|
|
130
|
+
sn >>= 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
r = await prefixedHash(0x01, r, p);
|
|
135
|
+
}
|
|
136
|
+
fn >>= 1;
|
|
137
|
+
sn >>= 1;
|
|
138
|
+
}
|
|
139
|
+
return sn === 0 && bytesHex(r) === root;
|
|
140
|
+
} catch {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The fold a batch's rule names. Unknown rules are refused, never folded as v1. */
|
|
146
|
+
export function treeRuleOf(value) {
|
|
147
|
+
if (value === undefined || value === null || value === "" || value === TREE_RULE_V1) return TREE_RULE_V1;
|
|
148
|
+
if (value === TREE_RULE_V2) return TREE_RULE_V2;
|
|
149
|
+
return null;
|
|
150
|
+
}
|