@xfuel/verify 0.3.1 → 0.3.3
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 +16 -6
- package/dist/anchor-trust.d.ts +129 -0
- package/dist/anchor-trust.d.ts.map +1 -0
- package/dist/anchor-trust.js +395 -0
- package/dist/anchor-trust.js.map +1 -0
- package/dist/anchor-witness.d.ts +114 -4
- package/dist/anchor-witness.d.ts.map +1 -1
- package/dist/anchor-witness.js +419 -32
- package/dist/anchor-witness.js.map +1 -1
- package/dist/carry-forward.d.ts +79 -0
- package/dist/carry-forward.d.ts.map +1 -0
- package/dist/carry-forward.js +184 -0
- package/dist/carry-forward.js.map +1 -0
- package/dist/cli.js +525 -41
- package/dist/cli.js.map +1 -1
- package/dist/epoch.d.ts +41 -7
- package/dist/epoch.d.ts.map +1 -1
- package/dist/epoch.js +143 -0
- package/dist/epoch.js.map +1 -1
- package/dist/index.d.ts +64 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +338 -96
- package/dist/index.js.map +1 -1
- package/dist/issuer-history.d.ts +19 -0
- package/dist/issuer-history.d.ts.map +1 -1
- package/dist/issuer-history.js +131 -75
- package/dist/issuer-history.js.map +1 -1
- package/dist/issuer-pin.d.ts +126 -0
- package/dist/issuer-pin.d.ts.map +1 -0
- package/dist/issuer-pin.js +567 -0
- package/dist/issuer-pin.js.map +1 -0
- package/dist/preimage.d.ts +29 -0
- package/dist/preimage.d.ts.map +1 -1
- package/dist/preimage.js +162 -2
- package/dist/preimage.js.map +1 -1
- package/dist/receipt-lane.d.ts +1 -0
- package/dist/receipt-lane.d.ts.map +1 -1
- package/dist/receipt-lane.js +5 -2
- package/dist/receipt-lane.js.map +1 -1
- package/dist/refusal.d.ts +5 -2
- package/dist/refusal.d.ts.map +1 -1
- package/dist/refusal.js +28 -6
- package/dist/refusal.js.map +1 -1
- package/dist/request-binding.d.ts +7 -0
- package/dist/request-binding.d.ts.map +1 -0
- package/dist/request-binding.js +26 -0
- package/dist/request-binding.js.map +1 -0
- package/dist/row-hash.d.ts +22 -0
- package/dist/row-hash.d.ts.map +1 -0
- package/dist/row-hash.js +26 -0
- package/dist/row-hash.js.map +1 -0
- package/dist/v11-receipt.d.ts +21 -0
- package/dist/v11-receipt.d.ts.map +1 -0
- package/dist/v11-receipt.js +351 -0
- package/dist/v11-receipt.js.map +1 -0
- package/package.json +1 -1
package/dist/anchor-witness.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ANCHOR_DOES_NOT_PROVE = exports.ANCHOR_PROVES = exports.BASE_MAINNET_CHAIN_ID = exports.SOLANA_GENESIS = exports.MEMO_PROGRAM_ID_V1 = exports.MEMO_PROGRAM_ID = void 0;
|
|
3
|
+
exports.NEWEST_ANCHOR_PAGE_CAP = exports.NEWEST_ANCHOR_PAGE_LIMIT = exports.ANCHOR_DOES_NOT_PROVE = exports.ANCHOR_PROVES = exports.BASE_MAINNET_CHAIN_ID = exports.SOLANA_GENESIS = exports.MEMO_PROGRAM_ID_V1 = exports.MEMO_PROGRAM_ID = void 0;
|
|
4
4
|
exports.leafHash = leafHash;
|
|
5
5
|
exports.normalizeRoot = normalizeRoot;
|
|
6
|
+
exports.inclusionRoot = inclusionRoot;
|
|
6
7
|
exports.verifyMerkleInclusion = verifyMerkleInclusion;
|
|
7
8
|
exports.parseAnchorMemo = parseAnchorMemo;
|
|
8
9
|
exports.extractMemos = extractMemos;
|
|
9
10
|
exports.fetchSolanaGenesisHash = fetchSolanaGenesisHash;
|
|
10
11
|
exports.fetchBaseAnchorTransaction = fetchBaseAnchorTransaction;
|
|
12
|
+
exports.fetchNewestSolanaAnchorRoot = fetchNewestSolanaAnchorRoot;
|
|
13
|
+
exports.solanaFeePayer = solanaFeePayer;
|
|
11
14
|
exports.verifyAnchoredRoot = verifyAnchoredRoot;
|
|
12
15
|
/**
|
|
13
16
|
* Check a receipt leaf against a signed tree head, then against the two
|
|
@@ -18,8 +21,11 @@ exports.verifyAnchoredRoot = verifyAnchoredRoot;
|
|
|
18
21
|
* the payment.
|
|
19
22
|
*/
|
|
20
23
|
const node_crypto_1 = require("node:crypto");
|
|
24
|
+
const anchor_trust_js_1 = require("./anchor-trust.js");
|
|
21
25
|
const base_payer_js_1 = require("./base-payer.js");
|
|
22
26
|
const epoch_js_1 = require("./epoch.js");
|
|
27
|
+
const issuer_pin_js_1 = require("./issuer-pin.js");
|
|
28
|
+
const row_hash_js_1 = require("./row-hash.js");
|
|
23
29
|
const solana_payer_js_1 = require("./solana-payer.js");
|
|
24
30
|
exports.MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
|
|
25
31
|
exports.MEMO_PROGRAM_ID_V1 = 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo';
|
|
@@ -35,15 +41,18 @@ exports.SOLANA_GENESIS = {
|
|
|
35
41
|
};
|
|
36
42
|
exports.BASE_MAINNET_CHAIN_ID = 8453;
|
|
37
43
|
exports.ANCHOR_PROVES = [
|
|
38
|
-
'This receipt leaf is inside the
|
|
39
|
-
'The
|
|
40
|
-
'The
|
|
44
|
+
'This receipt leaf is inside the tree of the stated size, under the stated root.',
|
|
45
|
+
'The tree head carries a valid ES256 issuer signature. The kid is trusted by the production pin, issuer history, or JWKS, and the signed root, size, epoch, and anchors match the head.',
|
|
46
|
+
'The Solana transaction memo contains that root, its fee payer is the signed fee payer, and the RPC genesis hash is the cluster named on the head.',
|
|
47
|
+
'The Base transaction calldata is that same root, on chain id 8453, and its sender is the signed anchor wallet.',
|
|
48
|
+
'Those wallets are on the issuer anchor-wallet list: the package pin, or a signed well-known or issuer-history list.',
|
|
41
49
|
];
|
|
42
50
|
exports.ANCHOR_DOES_NOT_PROVE = [
|
|
43
51
|
'It does not prove the payment, the payer, the payee, or the model output.',
|
|
44
52
|
'It does not prove a receipt appended after this tree size is inside the root.',
|
|
45
53
|
'It does not prove the issuer included only honest rows. It proves this leaf hashes into the published root.',
|
|
46
54
|
'It does not prove the RPC itself is honest. It checks the transaction and genesis hash that RPC returned.',
|
|
55
|
+
'It does not read the on-chain issuer-root registry or the DNS anchor. Those sources are reserved and are not consulted.',
|
|
47
56
|
];
|
|
48
57
|
function sha256(buf) {
|
|
49
58
|
return (0, node_crypto_1.createHash)('sha256').update(buf).digest();
|
|
@@ -58,19 +67,102 @@ function normalizeRoot(root) {
|
|
|
58
67
|
const hex = String(root || '').replace(/^0x/, '').toLowerCase();
|
|
59
68
|
return /^[0-9a-f]{64}$/.test(hex) ? hex : null;
|
|
60
69
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
function half(n) {
|
|
71
|
+
return Math.floor(n / 2);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* RFC 9162 fold. Returns the recomputed root, or null when the proof is the
|
|
75
|
+
* wrong length for `(index, treeSize)` or a sibling is not 32 bytes.
|
|
76
|
+
* Index and tree size choose the side. A `position` label is not read here.
|
|
77
|
+
*/
|
|
78
|
+
function inclusionRoot(leaf, index, treeSize, proof) {
|
|
79
|
+
if (!Array.isArray(proof))
|
|
80
|
+
return null;
|
|
81
|
+
const leafIndex = Number(index);
|
|
82
|
+
const size = Number(treeSize);
|
|
83
|
+
if (!Number.isSafeInteger(leafIndex) || !Number.isSafeInteger(size))
|
|
84
|
+
return null;
|
|
85
|
+
if (leafIndex < 0 || leafIndex >= size)
|
|
86
|
+
return null;
|
|
87
|
+
let fn = leafIndex;
|
|
88
|
+
let sn = size - 1;
|
|
89
|
+
let hash = Buffer.from(leaf);
|
|
66
90
|
for (const step of proof) {
|
|
91
|
+
if (sn === 0)
|
|
92
|
+
return null;
|
|
67
93
|
if (!step || !/^[0-9a-fA-F]{64}$/.test(step.hash))
|
|
68
|
-
return
|
|
94
|
+
return null;
|
|
69
95
|
const sib = Buffer.from(step.hash, 'hex');
|
|
70
|
-
const
|
|
71
|
-
|
|
96
|
+
const siblingOnLeft = (fn % 2) === 1 || fn === sn;
|
|
97
|
+
if (siblingOnLeft) {
|
|
98
|
+
hash = nodeHash(sib, hash);
|
|
99
|
+
if ((fn % 2) === 0) {
|
|
100
|
+
while ((fn % 2) === 0 && fn !== 0) {
|
|
101
|
+
fn = half(fn);
|
|
102
|
+
sn = half(sn);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
hash = nodeHash(hash, sib);
|
|
108
|
+
}
|
|
109
|
+
fn = half(fn);
|
|
110
|
+
sn = half(sn);
|
|
111
|
+
}
|
|
112
|
+
if (sn !== 0)
|
|
113
|
+
return null;
|
|
114
|
+
return hash.toString('hex');
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* A present `position` label must name the side `index` and `treeSize` already
|
|
118
|
+
* chose. An omitted label does not. This is the inclusion-proof check. The
|
|
119
|
+
* public audit path folds with `inclusionRoot` and does not read the label.
|
|
120
|
+
*/
|
|
121
|
+
function positionAgrees(index, treeSize, proof) {
|
|
122
|
+
if (!Array.isArray(proof))
|
|
123
|
+
return false;
|
|
124
|
+
const leafIndex = Number(index);
|
|
125
|
+
const size = Number(treeSize);
|
|
126
|
+
if (!Number.isSafeInteger(leafIndex) || !Number.isSafeInteger(size))
|
|
127
|
+
return false;
|
|
128
|
+
if (leafIndex < 0 || leafIndex >= size)
|
|
129
|
+
return false;
|
|
130
|
+
let fn = leafIndex;
|
|
131
|
+
let sn = size - 1;
|
|
132
|
+
for (const step of proof) {
|
|
133
|
+
if (sn === 0)
|
|
134
|
+
return false;
|
|
135
|
+
const siblingOnLeft = (fn % 2) === 1 || fn === sn;
|
|
136
|
+
if (typeof step?.position === 'string' && step.position !== '' && step.position !== (siblingOnLeft ? 'left' : 'right')) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if (siblingOnLeft && (fn % 2) === 0) {
|
|
140
|
+
while ((fn % 2) === 0 && fn !== 0) {
|
|
141
|
+
fn = half(fn);
|
|
142
|
+
sn = half(sn);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
fn = half(fn);
|
|
146
|
+
sn = half(sn);
|
|
72
147
|
}
|
|
73
|
-
return
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* RFC 9162 §2.1.3.2 inclusion. `index` and `treeSize` choose left or right
|
|
152
|
+
* at each step. A `position` label is not the source of that side. When a
|
|
153
|
+
* label is present it must name the side the index already chose, so a
|
|
154
|
+
* swapped label fails. An omitted label does not. A proof whose length is
|
|
155
|
+
* not the length that pair requires is rejected, as is `index >= treeSize`.
|
|
156
|
+
* Leaf and node bytes stay SHA-256(0x00 || leaf) and SHA-256(0x01 || left ||
|
|
157
|
+
* right). Consistency proofs and the empty root are not this function.
|
|
158
|
+
*/
|
|
159
|
+
function verifyMerkleInclusion(leaf, index, treeSize, rootHex, proof) {
|
|
160
|
+
const root = String(rootHex || '').replace(/^0x/, '').toLowerCase();
|
|
161
|
+
if (!/^[0-9a-f]{64}$/.test(root))
|
|
162
|
+
return false;
|
|
163
|
+
if (!positionAgrees(index, treeSize, proof))
|
|
164
|
+
return false;
|
|
165
|
+
return inclusionRoot(leaf, index, treeSize, proof) === root;
|
|
74
166
|
}
|
|
75
167
|
function memoIdentity(scope, day, root, prev) {
|
|
76
168
|
if (!/^[A-Za-z0-9_-]{1,64}$/.test(scope))
|
|
@@ -180,18 +272,132 @@ async function fetchBaseAnchorTransaction(txHash, rpcUrl) {
|
|
|
180
272
|
return {
|
|
181
273
|
hash: tx.hash,
|
|
182
274
|
input: tx.input,
|
|
275
|
+
from: tx.from,
|
|
183
276
|
chainId: chainHex ? Number(chainHex) : undefined,
|
|
184
277
|
};
|
|
185
278
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
279
|
+
/** Newest-first page size for `getSignaturesForAddress`. */
|
|
280
|
+
exports.NEWEST_ANCHOR_PAGE_LIMIT = 25;
|
|
281
|
+
/** Full pages walked before a missing memo is `newest_anchor_unavailable`. */
|
|
282
|
+
exports.NEWEST_ANCHOR_PAGE_CAP = 8;
|
|
283
|
+
function finiteNumber(value) {
|
|
284
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
285
|
+
}
|
|
286
|
+
function asSafeInt(value) {
|
|
287
|
+
if (typeof value === 'number')
|
|
288
|
+
return Number.isSafeInteger(value) ? value : null;
|
|
289
|
+
if (typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) {
|
|
290
|
+
const parsed = Number(value);
|
|
291
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Pending is a leaf past the anchored head. An index inside that head is
|
|
297
|
+
* `pending_leaf_in_head`. Any other broken bound is `pending_bounds`.
|
|
298
|
+
* `live_tree_size` is checked only when the inclusion names it.
|
|
299
|
+
*/
|
|
300
|
+
function pendingGeometryReason(inclusion) {
|
|
301
|
+
const size = asSafeInt(inclusion?.tree_size);
|
|
302
|
+
const index = asSafeInt(inclusion?.leaf_index);
|
|
303
|
+
if (size == null || index == null || size < 1)
|
|
304
|
+
return 'pending_bounds';
|
|
305
|
+
if (index < size)
|
|
306
|
+
return 'pending_leaf_in_head';
|
|
307
|
+
const liveRaw = inclusion?.live_tree_size;
|
|
308
|
+
if (liveRaw != null) {
|
|
309
|
+
const live = asSafeInt(liveRaw);
|
|
310
|
+
if (live == null || size >= live || index >= live)
|
|
311
|
+
return 'pending_bounds';
|
|
312
|
+
}
|
|
193
313
|
return null;
|
|
194
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Newest chit402 root memo paid by this fee payer. `getSignaturesForAddress`
|
|
317
|
+
* is newest-first and includes transactions that only mention the address.
|
|
318
|
+
* A memo whose fee payer is anyone else is ignored and is not newer.
|
|
319
|
+
* Pages use `before` until the first memo this payer actually paid. A listed
|
|
320
|
+
* signature whose transaction is null or whose fetch throws fails the scan.
|
|
321
|
+
* Walking `NEWEST_ANCHOR_PAGE_CAP` full pages without such a memo fails the scan.
|
|
322
|
+
* A short page with no memo from this payer means this payer has no anchor.
|
|
323
|
+
*/
|
|
324
|
+
async function fetchNewestSolanaAnchorRoot(feePayer, rpcUrl, fetchTx) {
|
|
325
|
+
const load = fetchTx || ((signature, url) => (0, solana_payer_js_1.fetchSolanaTransaction)(signature, url));
|
|
326
|
+
let before;
|
|
327
|
+
for (let page = 0; page < exports.NEWEST_ANCHOR_PAGE_CAP; page += 1) {
|
|
328
|
+
const query = { limit: exports.NEWEST_ANCHOR_PAGE_LIMIT };
|
|
329
|
+
if (before)
|
|
330
|
+
query.before = before;
|
|
331
|
+
const listed = await defaultRpc(rpcUrl, 'getSignaturesForAddress', [feePayer, query]);
|
|
332
|
+
if (!Array.isArray(listed))
|
|
333
|
+
throw new Error('newest_anchor_unavailable');
|
|
334
|
+
if (listed.length === 0)
|
|
335
|
+
return null;
|
|
336
|
+
for (const row of listed) {
|
|
337
|
+
if (!row || typeof row !== 'object')
|
|
338
|
+
continue;
|
|
339
|
+
const record = row;
|
|
340
|
+
if (record.err)
|
|
341
|
+
continue;
|
|
342
|
+
if (typeof record.signature !== 'string' || record.signature.length === 0)
|
|
343
|
+
continue;
|
|
344
|
+
let tx;
|
|
345
|
+
try {
|
|
346
|
+
tx = await load(record.signature, rpcUrl);
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
throw new Error('newest_anchor_unavailable');
|
|
350
|
+
}
|
|
351
|
+
if (!tx)
|
|
352
|
+
throw new Error('newest_anchor_unavailable');
|
|
353
|
+
if (solanaFeePayer(tx) !== feePayer)
|
|
354
|
+
continue;
|
|
355
|
+
for (const memo of extractMemos(tx)) {
|
|
356
|
+
const parsed = parseAnchorMemo(memo);
|
|
357
|
+
if (!parsed?.root)
|
|
358
|
+
continue;
|
|
359
|
+
const rowSlot = finiteNumber(record.slot);
|
|
360
|
+
return {
|
|
361
|
+
root: parsed.root,
|
|
362
|
+
signature: record.signature,
|
|
363
|
+
slot: rowSlot != null ? rowSlot : finiteNumber(tx.slot),
|
|
364
|
+
blockTime: finiteNumber(record.blockTime),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (listed.length < exports.NEWEST_ANCHOR_PAGE_LIMIT)
|
|
369
|
+
return null;
|
|
370
|
+
const tail = listed[listed.length - 1];
|
|
371
|
+
if (typeof tail?.signature !== 'string' || tail.signature.length === 0) {
|
|
372
|
+
throw new Error('newest_anchor_unavailable');
|
|
373
|
+
}
|
|
374
|
+
if (page + 1 === exports.NEWEST_ANCHOR_PAGE_CAP)
|
|
375
|
+
throw new Error('newest_anchor_unavailable');
|
|
376
|
+
before = tail.signature;
|
|
377
|
+
}
|
|
378
|
+
throw new Error('newest_anchor_unavailable');
|
|
379
|
+
}
|
|
380
|
+
/** Fee payer is the first account. A parsed key that is explicitly not a signer does not count. */
|
|
381
|
+
function solanaFeePayer(tx) {
|
|
382
|
+
const keys = tx?.transaction?.message?.accountKeys || [];
|
|
383
|
+
if (!keys.length)
|
|
384
|
+
return null;
|
|
385
|
+
const first = keys[0];
|
|
386
|
+
if (typeof first === 'string')
|
|
387
|
+
return first;
|
|
388
|
+
if (first?.signer === false)
|
|
389
|
+
return null;
|
|
390
|
+
return first?.pubkey || null;
|
|
391
|
+
}
|
|
392
|
+
function rowHashFrom(receipt, inclusion) {
|
|
393
|
+
const bound = (0, row_hash_js_1.boundRowHash)(receipt, inclusion);
|
|
394
|
+
if (!bound.ok)
|
|
395
|
+
return { reason: 'row_hash_mismatch' };
|
|
396
|
+
if (bound.row == null || bound.row === '')
|
|
397
|
+
return null;
|
|
398
|
+
const fromReceipt = typeof receipt.row_hash === 'string' || typeof receipt.book_chain?.row_hash === 'string';
|
|
399
|
+
return { rowHash: bound.row, source: fromReceipt ? 'receipt' : 'inclusion' };
|
|
400
|
+
}
|
|
195
401
|
function solanaSignature(head) {
|
|
196
402
|
const sol = head.anchors?.solana;
|
|
197
403
|
if (!sol || sol.status === 'pending')
|
|
@@ -218,7 +424,24 @@ async function verifyAnchoredRoot(input) {
|
|
|
218
424
|
let inclusionReason;
|
|
219
425
|
const taskId = input.receipt?.task_id ? String(input.receipt.task_id) : '';
|
|
220
426
|
const inclusionError = input.inclusion?.error;
|
|
221
|
-
|
|
427
|
+
const pendingLeaf = input.inclusion?.status === 'pending_anchor';
|
|
428
|
+
if (pendingLeaf) {
|
|
429
|
+
if (!taskId)
|
|
430
|
+
inclusionReason = 'missing_task_id';
|
|
431
|
+
else if (input.inclusion?.task_id && String(input.inclusion.task_id) !== taskId)
|
|
432
|
+
inclusionReason = 'task_mismatch';
|
|
433
|
+
else if (!root)
|
|
434
|
+
inclusionReason = 'bad_root';
|
|
435
|
+
else if (inclusionRoot && inclusionRoot !== root)
|
|
436
|
+
inclusionReason = 'root_mismatch';
|
|
437
|
+
else if (input.head.tree_size != null && input.inclusion.tree_size != null
|
|
438
|
+
&& Number(input.head.tree_size) !== Number(input.inclusion.tree_size)) {
|
|
439
|
+
inclusionReason = 'tree_size_mismatch';
|
|
440
|
+
}
|
|
441
|
+
else
|
|
442
|
+
inclusionReason = pendingGeometryReason(input.inclusion) || 'pending_anchor';
|
|
443
|
+
}
|
|
444
|
+
else if (inclusionError === 'not_in_tree') {
|
|
222
445
|
inclusionReason = 'not_in_tree';
|
|
223
446
|
}
|
|
224
447
|
else if (!taskId) {
|
|
@@ -239,7 +462,10 @@ async function verifyAnchoredRoot(input) {
|
|
|
239
462
|
else {
|
|
240
463
|
const row = rowHashFrom(input.receipt, input.inclusion);
|
|
241
464
|
let leaf = null;
|
|
242
|
-
if (row) {
|
|
465
|
+
if (row && 'reason' in row) {
|
|
466
|
+
inclusionReason = row.reason;
|
|
467
|
+
}
|
|
468
|
+
else if (row) {
|
|
243
469
|
leaf = leafHash(Buffer.from(`${taskId}|${row.rowHash}`));
|
|
244
470
|
leafSource = row.source;
|
|
245
471
|
if (input.inclusion.leaf && input.inclusion.leaf.toLowerCase() !== leaf.toString('hex')) {
|
|
@@ -247,11 +473,6 @@ async function verifyAnchoredRoot(input) {
|
|
|
247
473
|
leaf = null;
|
|
248
474
|
}
|
|
249
475
|
}
|
|
250
|
-
else if (input.inclusion.leaf && /^[0-9a-fA-F]{64}$/.test(input.inclusion.leaf)) {
|
|
251
|
-
leaf = Buffer.from(input.inclusion.leaf, 'hex');
|
|
252
|
-
leafSource = 'inclusion';
|
|
253
|
-
doesNotProve.push('The leaf was taken from the inclusion object. The receipt had no row_hash, so this check did not recompute the leaf from the receipt bytes.');
|
|
254
|
-
}
|
|
255
476
|
else {
|
|
256
477
|
inclusionReason = 'no_leaf';
|
|
257
478
|
}
|
|
@@ -262,8 +483,9 @@ async function verifyAnchoredRoot(input) {
|
|
|
262
483
|
inclusionReason = 'inclusion_failed';
|
|
263
484
|
}
|
|
264
485
|
}
|
|
265
|
-
if (!inclusionValid && inclusionReason)
|
|
486
|
+
if (!(pendingLeaf && inclusionReason === 'pending_anchor') && !inclusionValid && inclusionReason) {
|
|
266
487
|
errors.push(inclusionReason);
|
|
488
|
+
}
|
|
267
489
|
let epochReason;
|
|
268
490
|
if (input.head?.epoch == null) {
|
|
269
491
|
const link = (0, epoch_js_1.verifyEpochLink)(input.head, null);
|
|
@@ -296,6 +518,35 @@ async function verifyAnchoredRoot(input) {
|
|
|
296
518
|
}
|
|
297
519
|
if (epochReason)
|
|
298
520
|
errors.push(epochReason);
|
|
521
|
+
const headTrust = (0, anchor_trust_js_1.verifyTreeHeadTrust)(input.head, {
|
|
522
|
+
jwks: input.jwks,
|
|
523
|
+
trustedKids: input.trustedKids,
|
|
524
|
+
issuerHistory: input.issuerHistory ?? null,
|
|
525
|
+
strictIssuerHistory: input.strictIssuerHistory === true,
|
|
526
|
+
});
|
|
527
|
+
if (!headTrust.ok && headTrust.reason)
|
|
528
|
+
errors.push(headTrust.reason);
|
|
529
|
+
const legacyHead = headTrust.ok && (0, anchor_trust_js_1.headOmitsPinnedSigner)(headTrust.payload);
|
|
530
|
+
if (legacyHead)
|
|
531
|
+
errors.push(anchor_trust_js_1.LEGACY_HEAD_UNPINNED_SIGNER);
|
|
532
|
+
let publishedWallets = null;
|
|
533
|
+
if (input.anchorWalletDocument) {
|
|
534
|
+
const listed = (0, anchor_trust_js_1.verifyAnchorWalletDocument)(input.anchorWalletDocument, {
|
|
535
|
+
jwks: input.jwks,
|
|
536
|
+
trustedKids: input.trustedKids,
|
|
537
|
+
});
|
|
538
|
+
if (!listed.ok)
|
|
539
|
+
errors.push(listed.reason);
|
|
540
|
+
else
|
|
541
|
+
publishedWallets = { base: listed.base, solana: listed.solana };
|
|
542
|
+
}
|
|
543
|
+
const walletList = (0, anchor_trust_js_1.compileAnchorWallets)({
|
|
544
|
+
jwks: input.jwks,
|
|
545
|
+
trustedKids: input.trustedKids,
|
|
546
|
+
issuerHistory: headTrust.ok ? (input.issuerHistory ?? null) : null,
|
|
547
|
+
published: publishedWallets,
|
|
548
|
+
});
|
|
549
|
+
const anchorWalletsInvalid = errors.includes('anchor_wallets_invalid');
|
|
299
550
|
const solanaRpc = input.solanaRpcUrl || process.env.SOLANA_RPC_URL || solana_payer_js_1.SOLANA_RPC_URL;
|
|
300
551
|
const baseRpc = input.baseRpcUrl || base_payer_js_1.BASE_RPC_URL;
|
|
301
552
|
const signature = root && inclusionValid ? solanaSignature(input.head) : null;
|
|
@@ -307,6 +558,7 @@ async function verifyAnchoredRoot(input) {
|
|
|
307
558
|
slot: input.head?.anchors?.solana?.slot ?? null,
|
|
308
559
|
cluster: input.head?.anchors?.solana?.cluster ?? null,
|
|
309
560
|
memo: input.head?.anchors?.solana?.memo ?? null,
|
|
561
|
+
fee_payer: input.head?.anchors?.solana?.fee_payer ?? null,
|
|
310
562
|
reason: undefined,
|
|
311
563
|
};
|
|
312
564
|
const base = {
|
|
@@ -314,17 +566,53 @@ async function verifyAnchoredRoot(input) {
|
|
|
314
566
|
valid: false,
|
|
315
567
|
tx: input.head?.anchors?.base?.tx || input.head?.anchor?.tx || input.head?.anchor_tx || null,
|
|
316
568
|
chain_id: input.head?.anchors?.base?.chain_id ?? input.head?.anchor?.chain_id ?? null,
|
|
569
|
+
from: input.head?.anchors?.base?.from ?? null,
|
|
317
570
|
reason: undefined,
|
|
318
571
|
};
|
|
319
|
-
|
|
572
|
+
const signedFrom = input.head?.anchors?.base?.from || null;
|
|
573
|
+
const signedPayer = input.head?.anchors?.solana?.fee_payer || null;
|
|
574
|
+
let walletFailed = anchorWalletsInvalid;
|
|
575
|
+
if (signedFrom && !(0, anchor_trust_js_1.walletListed)(walletList.base, signedFrom, 'base')) {
|
|
576
|
+
walletFailed = true;
|
|
577
|
+
errors.push('anchor_sender_unlisted');
|
|
578
|
+
}
|
|
579
|
+
if (signedPayer && !(0, anchor_trust_js_1.walletListed)(walletList.solana, signedPayer, 'solana')) {
|
|
580
|
+
walletFailed = true;
|
|
581
|
+
errors.push('fee_payer_unlisted');
|
|
582
|
+
}
|
|
583
|
+
const claimsAnchored = input.inclusion?.status === 'anchored' || input.inclusion?.anchor_status === 'anchored';
|
|
584
|
+
const headHasAnchor = Boolean(solanaSignature(input.head) || baseTxHash(input.head));
|
|
585
|
+
if (!headHasAnchor && (claimsAnchored || (pendingLeaf && inclusionReason === 'pending_anchor'))) {
|
|
586
|
+
errors.push('head_not_anchored');
|
|
587
|
+
}
|
|
588
|
+
if (pendingLeaf && inclusionReason === 'pending_anchor') {
|
|
589
|
+
solana.reason = 'pending_anchor';
|
|
590
|
+
base.reason = 'pending_anchor';
|
|
591
|
+
}
|
|
592
|
+
else if (!inclusionValid) {
|
|
320
593
|
solana.reason = 'inclusion_failed';
|
|
321
594
|
base.reason = 'inclusion_failed';
|
|
322
595
|
}
|
|
596
|
+
else if (!headTrust.ok || walletFailed) {
|
|
597
|
+
const reason = !headTrust.ok
|
|
598
|
+
? (headTrust.reason || 'head_signature_invalid')
|
|
599
|
+
: (errors.find((code) => code.startsWith('anchor_') || code.startsWith('fee_payer')) || 'anchor_wallets_invalid');
|
|
600
|
+
solana.reason = solana.fee_payer && errors.includes('fee_payer_unlisted') ? 'fee_payer_unlisted' : reason;
|
|
601
|
+
base.reason = base.from && errors.includes('anchor_sender_unlisted') ? 'anchor_sender_unlisted' : reason;
|
|
602
|
+
}
|
|
603
|
+
else if (legacyHead) {
|
|
604
|
+
solana.reason = anchor_trust_js_1.LEGACY_HEAD_UNPINNED_SIGNER;
|
|
605
|
+
base.reason = anchor_trust_js_1.LEGACY_HEAD_UNPINNED_SIGNER;
|
|
606
|
+
}
|
|
323
607
|
else {
|
|
324
608
|
if (!signature) {
|
|
325
609
|
solana.reason = 'pending';
|
|
326
610
|
errors.push('solana_pending');
|
|
327
611
|
}
|
|
612
|
+
else if (!signedPayer) {
|
|
613
|
+
solana.reason = 'fee_payer_missing';
|
|
614
|
+
errors.push('fee_payer_missing');
|
|
615
|
+
}
|
|
328
616
|
else {
|
|
329
617
|
solana.checked = true;
|
|
330
618
|
try {
|
|
@@ -361,6 +649,10 @@ async function verifyAnchoredRoot(input) {
|
|
|
361
649
|
if (!solana.reason && headSlot != null && tx.slot != null && Number(tx.slot) !== Number(headSlot)) {
|
|
362
650
|
solana.reason = 'slot_mismatch';
|
|
363
651
|
}
|
|
652
|
+
const payer = solanaFeePayer(tx);
|
|
653
|
+
if (!solana.reason && payer !== signedPayer) {
|
|
654
|
+
solana.reason = 'fee_payer_mismatch';
|
|
655
|
+
}
|
|
364
656
|
if (!solana.reason) {
|
|
365
657
|
solana.valid = true;
|
|
366
658
|
solana.memo = matched[0];
|
|
@@ -379,6 +671,10 @@ async function verifyAnchoredRoot(input) {
|
|
|
379
671
|
base.reason = 'pending';
|
|
380
672
|
errors.push('base_pending');
|
|
381
673
|
}
|
|
674
|
+
else if (!signedFrom) {
|
|
675
|
+
base.reason = 'anchor_sender_missing';
|
|
676
|
+
errors.push('anchor_sender_missing');
|
|
677
|
+
}
|
|
382
678
|
else {
|
|
383
679
|
base.checked = true;
|
|
384
680
|
try {
|
|
@@ -396,6 +692,8 @@ async function verifyAnchoredRoot(input) {
|
|
|
396
692
|
const inputData = String(tx.input || '').toLowerCase().replace(/^0x/, '');
|
|
397
693
|
if (inputData !== root)
|
|
398
694
|
base.reason = 'calldata_mismatch';
|
|
695
|
+
else if (!tx.from || !(0, anchor_trust_js_1.sameBaseAddress)(tx.from, signedFrom))
|
|
696
|
+
base.reason = 'sender_mismatch';
|
|
399
697
|
else {
|
|
400
698
|
base.valid = true;
|
|
401
699
|
base.tx = tx.hash || txHash;
|
|
@@ -410,22 +708,111 @@ async function verifyAnchoredRoot(input) {
|
|
|
410
708
|
errors.push(`base:${base.reason}`);
|
|
411
709
|
}
|
|
412
710
|
}
|
|
711
|
+
const pinInput = input.issuerPin || {};
|
|
712
|
+
const issuerPin = (0, issuer_pin_js_1.assessIssuerPin)({
|
|
713
|
+
...pinInput,
|
|
714
|
+
receipt: pinInput.receipt ?? input.receipt,
|
|
715
|
+
head: pinInput.head ?? input.head,
|
|
716
|
+
required: pinInput.required === true || input.issuerPin != null,
|
|
717
|
+
});
|
|
718
|
+
if (issuerPin.checked && !issuerPin.ok && issuerPin.code)
|
|
719
|
+
errors.push(issuerPin.code);
|
|
720
|
+
const pinFailed = issuerPin.checked && !issuerPin.ok;
|
|
721
|
+
let staleReason;
|
|
722
|
+
const claimsAnchor = Boolean(solanaSignature(input.head) || baseTxHash(input.head));
|
|
723
|
+
if (input.checkNewestAnchor === true && claimsAnchor) {
|
|
724
|
+
const payerOnList = Boolean(signedPayer && (0, anchor_trust_js_1.walletListed)(walletList.solana, signedPayer, 'solana'));
|
|
725
|
+
if (!signedPayer || !root || !payerOnList) {
|
|
726
|
+
staleReason = 'newest_anchor_unavailable';
|
|
727
|
+
}
|
|
728
|
+
else {
|
|
729
|
+
try {
|
|
730
|
+
const found = input.fetchNewestAnchor
|
|
731
|
+
? await input.fetchNewestAnchor(signedPayer, solanaRpc)
|
|
732
|
+
: await fetchNewestSolanaAnchorRoot(signedPayer, solanaRpc, input.fetchSolanaTx);
|
|
733
|
+
if (!found?.root)
|
|
734
|
+
staleReason = 'newest_anchor_unavailable';
|
|
735
|
+
else if (found.root !== root)
|
|
736
|
+
staleReason = 'stale_head';
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
staleReason = 'newest_anchor_unavailable';
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (staleReason)
|
|
743
|
+
errors.push(staleReason);
|
|
744
|
+
}
|
|
413
745
|
let overall;
|
|
414
|
-
|
|
746
|
+
const anchorSideFailed = (solana.reason && solana.reason !== 'pending' && solana.reason !== 'pending_anchor')
|
|
747
|
+
|| (base.reason && base.reason !== 'pending' && base.reason !== 'pending_anchor');
|
|
748
|
+
const pendingOk = pendingLeaf
|
|
749
|
+
&& inclusionReason === 'pending_anchor'
|
|
750
|
+
&& headTrust.ok
|
|
751
|
+
&& !walletFailed
|
|
752
|
+
&& !legacyHead
|
|
753
|
+
&& !epochReason
|
|
754
|
+
&& !staleReason
|
|
755
|
+
&& !pinFailed
|
|
756
|
+
&& headHasAnchor
|
|
757
|
+
&& !errors.includes('head_not_anchored');
|
|
758
|
+
if (pendingOk)
|
|
759
|
+
overall = 'pending';
|
|
760
|
+
else if (!inclusionValid
|
|
761
|
+
|| epochReason
|
|
762
|
+
|| !headTrust.ok
|
|
763
|
+
|| walletFailed
|
|
764
|
+
|| legacyHead
|
|
765
|
+
|| anchorSideFailed
|
|
766
|
+
|| staleReason
|
|
767
|
+
|| errors.includes('head_not_anchored')
|
|
768
|
+
|| (solana.checked && !solana.valid)
|
|
769
|
+
|| (base.checked && !base.valid)
|
|
770
|
+
|| pinFailed)
|
|
415
771
|
overall = 'failed';
|
|
416
772
|
else if (solana.valid && base.valid)
|
|
417
773
|
overall = 'verified';
|
|
418
774
|
else
|
|
419
775
|
overall = 'partial';
|
|
776
|
+
let unloggedReason;
|
|
777
|
+
if (!inclusionValid && !epochReason) {
|
|
778
|
+
const listed = (0, epoch_js_1.unloggedReasonForTask)(input.epochRecord, taskId);
|
|
779
|
+
if (listed)
|
|
780
|
+
unloggedReason = listed.reason;
|
|
781
|
+
}
|
|
420
782
|
return {
|
|
421
783
|
overall,
|
|
422
784
|
root,
|
|
423
|
-
inclusion: {
|
|
785
|
+
inclusion: {
|
|
786
|
+
valid: inclusionValid,
|
|
787
|
+
leaf: leafHex,
|
|
788
|
+
leaf_source: leafSource,
|
|
789
|
+
reason: inclusionReason,
|
|
790
|
+
...(unloggedReason ? { unlogged_reason: unloggedReason } : {}),
|
|
791
|
+
...(input.inclusion?.anchored_tree_size != null ? { anchored_tree_size: input.inclusion.anchored_tree_size } : {}),
|
|
792
|
+
...(input.inclusion?.live_tree_size != null ? { live_tree_size: input.inclusion.live_tree_size } : {}),
|
|
793
|
+
},
|
|
424
794
|
solana,
|
|
425
795
|
base,
|
|
426
|
-
|
|
796
|
+
head_signature: {
|
|
797
|
+
checked: Boolean(input.head?.issuer_signature?.jws) || !headTrust.ok,
|
|
798
|
+
valid: headTrust.ok,
|
|
799
|
+
kid: headTrust.kid,
|
|
800
|
+
trust: headTrust.trust,
|
|
801
|
+
reason: headTrust.reason || undefined,
|
|
802
|
+
message: headTrust.message || undefined,
|
|
803
|
+
},
|
|
804
|
+
anchor_wallets: {
|
|
805
|
+
base_from: signedFrom,
|
|
806
|
+
solana_fee_payer: signedPayer,
|
|
807
|
+
sources: walletList.sources,
|
|
808
|
+
not_consulted: walletList.not_consulted,
|
|
809
|
+
},
|
|
810
|
+
proves: overall === 'pending'
|
|
811
|
+
? ['This leaf is past the anchored tree size. It waits for the next anchor.']
|
|
812
|
+
: exports.ANCHOR_PROVES,
|
|
427
813
|
does_not_prove: doesNotProve,
|
|
428
814
|
errors,
|
|
815
|
+
issuer_pin: issuerPin,
|
|
429
816
|
};
|
|
430
817
|
}
|
|
431
818
|
//# sourceMappingURL=anchor-witness.js.map
|