@xfuel/verify 0.3.0 → 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.
Files changed (56) hide show
  1. package/README.md +37 -16
  2. package/dist/anchor-trust.d.ts +129 -0
  3. package/dist/anchor-trust.d.ts.map +1 -0
  4. package/dist/anchor-trust.js +395 -0
  5. package/dist/anchor-trust.js.map +1 -0
  6. package/dist/anchor-witness.d.ts +134 -7
  7. package/dist/anchor-witness.d.ts.map +1 -1
  8. package/dist/anchor-witness.js +497 -44
  9. package/dist/anchor-witness.js.map +1 -1
  10. package/dist/carry-forward.d.ts +79 -0
  11. package/dist/carry-forward.d.ts.map +1 -0
  12. package/dist/carry-forward.js +184 -0
  13. package/dist/carry-forward.js.map +1 -0
  14. package/dist/cli.js +674 -30
  15. package/dist/cli.js.map +1 -1
  16. package/dist/epoch.d.ts +143 -0
  17. package/dist/epoch.d.ts.map +1 -0
  18. package/dist/epoch.js +352 -0
  19. package/dist/epoch.js.map +1 -0
  20. package/dist/index.d.ts +86 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +395 -118
  23. package/dist/index.js.map +1 -1
  24. package/dist/issuer-history.d.ts +19 -0
  25. package/dist/issuer-history.d.ts.map +1 -1
  26. package/dist/issuer-history.js +131 -75
  27. package/dist/issuer-history.js.map +1 -1
  28. package/dist/issuer-pin.d.ts +126 -0
  29. package/dist/issuer-pin.d.ts.map +1 -0
  30. package/dist/issuer-pin.js +567 -0
  31. package/dist/issuer-pin.js.map +1 -0
  32. package/dist/preimage.d.ts +29 -0
  33. package/dist/preimage.d.ts.map +1 -1
  34. package/dist/preimage.js +162 -2
  35. package/dist/preimage.js.map +1 -1
  36. package/dist/receipt-lane.d.ts +1 -0
  37. package/dist/receipt-lane.d.ts.map +1 -1
  38. package/dist/receipt-lane.js +5 -2
  39. package/dist/receipt-lane.js.map +1 -1
  40. package/dist/refusal.d.ts +5 -2
  41. package/dist/refusal.d.ts.map +1 -1
  42. package/dist/refusal.js +28 -6
  43. package/dist/refusal.js.map +1 -1
  44. package/dist/request-binding.d.ts +7 -0
  45. package/dist/request-binding.d.ts.map +1 -0
  46. package/dist/request-binding.js +26 -0
  47. package/dist/request-binding.js.map +1 -0
  48. package/dist/row-hash.d.ts +22 -0
  49. package/dist/row-hash.d.ts.map +1 -0
  50. package/dist/row-hash.js +26 -0
  51. package/dist/row-hash.js.map +1 -0
  52. package/dist/v11-receipt.d.ts +21 -0
  53. package/dist/v11-receipt.d.ts.map +1 -0
  54. package/dist/v11-receipt.js +351 -0
  55. package/dist/v11-receipt.js.map +1 -0
  56. package/package.json +1 -1
@@ -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,27 +21,38 @@ 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");
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");
22
29
  const solana_payer_js_1 = require("./solana-payer.js");
23
30
  exports.MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
24
31
  exports.MEMO_PROGRAM_ID_V1 = 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo';
25
- /** Genesis hashes from the Solana cluster CAIP-2 references. */
32
+ /**
33
+ * Full `getGenesisHash` values. CAIP-2 references keep only the first 32
34
+ * characters, and `getGenesisHash` returns the whole base58 hash. A prefix
35
+ * compare never matches mainnet-beta.
36
+ */
26
37
  exports.SOLANA_GENESIS = {
27
- 'mainnet-beta': '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
38
+ 'mainnet-beta': '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d',
28
39
  devnet: 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG',
29
40
  testnet: '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY',
30
41
  };
31
42
  exports.BASE_MAINNET_CHAIN_ID = 8453;
32
43
  exports.ANCHOR_PROVES = [
33
- 'This receipt leaf is inside the issuer append-only tree of the stated size, under the stated root.',
34
- 'The Solana transaction memo contains that root, and the RPC genesis hash is the cluster named on the head.',
35
- 'The Base transaction calldata is that same root, on chain id 8453.',
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.',
36
49
  ];
37
50
  exports.ANCHOR_DOES_NOT_PROVE = [
38
51
  'It does not prove the payment, the payer, the payee, or the model output.',
39
52
  'It does not prove a receipt appended after this tree size is inside the root.',
40
53
  'It does not prove the issuer included only honest rows. It proves this leaf hashes into the published root.',
41
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.',
42
56
  ];
43
57
  function sha256(buf) {
44
58
  return (0, node_crypto_1.createHash)('sha256').update(buf).digest();
@@ -53,30 +67,104 @@ function normalizeRoot(root) {
53
67
  const hex = String(root || '').replace(/^0x/, '').toLowerCase();
54
68
  return /^[0-9a-f]{64}$/.test(hex) ? hex : null;
55
69
  }
56
- /** RFC 6962-style inclusion. Same rule as the gateway tree. */
57
- function verifyMerkleInclusion(leaf, index, treeSize, rootHex, proof) {
58
- if (!Array.isArray(proof) || index < 0 || index >= treeSize)
59
- return false;
60
- let hash = new Uint8Array(leaf);
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);
61
90
  for (const step of proof) {
91
+ if (sn === 0)
92
+ return null;
62
93
  if (!step || !/^[0-9a-fA-F]{64}$/.test(step.hash))
63
- return false;
94
+ return null;
64
95
  const sib = Buffer.from(step.hash, 'hex');
65
- const next = step.position === 'left' ? nodeHash(sib, hash) : nodeHash(hash, sib);
66
- hash = new Uint8Array(next);
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);
67
111
  }
68
- return Buffer.from(hash).toString('hex') === String(rootHex).replace(/^0x/, '').toLowerCase();
69
- }
70
- function parseAnchorMemo(memo) {
71
- const parts = String(memo || '').split(':');
72
- if (parts.length !== 7)
73
- return null;
74
- if (parts[0] !== 'chit402' || parts[1] !== 'root' || parts[2] !== 'v1')
112
+ if (sn !== 0)
75
113
  return null;
76
- const scope = parts[3];
77
- const day = parts[4];
78
- const root = parts[5];
79
- const prev = parts[6];
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);
147
+ }
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;
166
+ }
167
+ function memoIdentity(scope, day, root, prev) {
80
168
  if (!/^[A-Za-z0-9_-]{1,64}$/.test(scope))
81
169
  return null;
82
170
  if (!/^\d{4}-\d{2}-\d{2}$/.test(day))
@@ -85,6 +173,40 @@ function parseAnchorMemo(memo) {
85
173
  return null;
86
174
  return { scope, day, root, prev };
87
175
  }
176
+ /** v1 memos stay valid. v2 adds epoch, prev_epoch_root, prev_epoch_size, and the bundle index hash. */
177
+ function parseAnchorMemo(memo) {
178
+ const parts = String(memo || '').split(':');
179
+ if (parts[0] !== 'chit402' || parts[1] !== 'root')
180
+ return null;
181
+ if (parts.length === 7 && parts[2] === 'v1') {
182
+ const base = memoIdentity(parts[3], parts[4], parts[5], parts[6]);
183
+ return base ? { version: 1, ...base } : null;
184
+ }
185
+ if (parts.length === 11 && parts[2] === 'v2') {
186
+ const base = memoIdentity(parts[3], parts[4], parts[5], parts[6]);
187
+ if (!base)
188
+ return null;
189
+ const epoch = Number(parts[7]);
190
+ const prevEpoch = parts[8];
191
+ const size = Number(parts[9]);
192
+ const bundle = parts[10];
193
+ if (!Number.isInteger(epoch) || epoch < 1)
194
+ return null;
195
+ if (!/^[0-9a-f]{64}$/.test(prevEpoch) || !/^[0-9a-f]{64}$/.test(bundle))
196
+ return null;
197
+ if (!Number.isInteger(size) || size < 0)
198
+ return null;
199
+ return {
200
+ version: 2,
201
+ ...base,
202
+ epoch,
203
+ prev_epoch_root: prevEpoch,
204
+ prev_epoch_size: size,
205
+ bundle_index_hash: bundle,
206
+ };
207
+ }
208
+ return null;
209
+ }
88
210
  function isMemoIx(ix) {
89
211
  const program = ix.program || '';
90
212
  const programId = ix.programId || '';
@@ -150,18 +272,132 @@ async function fetchBaseAnchorTransaction(txHash, rpcUrl) {
150
272
  return {
151
273
  hash: tx.hash,
152
274
  input: tx.input,
275
+ from: tx.from,
153
276
  chainId: chainHex ? Number(chainHex) : undefined,
154
277
  };
155
278
  }
156
- function rowHashFrom(receipt, inclusion) {
157
- if (typeof receipt.row_hash === 'string')
158
- return { rowHash: receipt.row_hash, source: 'receipt' };
159
- if (typeof receipt.book_chain?.row_hash === 'string')
160
- return { rowHash: receipt.book_chain.row_hash, source: 'receipt' };
161
- if (typeof inclusion.row_hash === 'string')
162
- return { rowHash: inclusion.row_hash, source: 'inclusion' };
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
+ }
163
313
  return null;
164
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
+ }
165
401
  function solanaSignature(head) {
166
402
  const sol = head.anchors?.solana;
167
403
  if (!sol || sol.status === 'pending')
@@ -187,7 +423,28 @@ async function verifyAnchoredRoot(input) {
187
423
  let inclusionValid = false;
188
424
  let inclusionReason;
189
425
  const taskId = input.receipt?.task_id ? String(input.receipt.task_id) : '';
190
- if (!taskId) {
426
+ const inclusionError = input.inclusion?.error;
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') {
445
+ inclusionReason = 'not_in_tree';
446
+ }
447
+ else if (!taskId) {
191
448
  inclusionReason = 'missing_task_id';
192
449
  }
193
450
  else if (input.inclusion?.task_id && String(input.inclusion.task_id) !== taskId) {
@@ -205,7 +462,10 @@ async function verifyAnchoredRoot(input) {
205
462
  else {
206
463
  const row = rowHashFrom(input.receipt, input.inclusion);
207
464
  let leaf = null;
208
- if (row) {
465
+ if (row && 'reason' in row) {
466
+ inclusionReason = row.reason;
467
+ }
468
+ else if (row) {
209
469
  leaf = leafHash(Buffer.from(`${taskId}|${row.rowHash}`));
210
470
  leafSource = row.source;
211
471
  if (input.inclusion.leaf && input.inclusion.leaf.toLowerCase() !== leaf.toString('hex')) {
@@ -213,11 +473,6 @@ async function verifyAnchoredRoot(input) {
213
473
  leaf = null;
214
474
  }
215
475
  }
216
- else if (input.inclusion.leaf && /^[0-9a-fA-F]{64}$/.test(input.inclusion.leaf)) {
217
- leaf = Buffer.from(input.inclusion.leaf, 'hex');
218
- leafSource = 'inclusion';
219
- 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.');
220
- }
221
476
  else {
222
477
  inclusionReason = 'no_leaf';
223
478
  }
@@ -228,8 +483,70 @@ async function verifyAnchoredRoot(input) {
228
483
  inclusionReason = 'inclusion_failed';
229
484
  }
230
485
  }
231
- if (!inclusionValid && inclusionReason)
486
+ if (!(pendingLeaf && inclusionReason === 'pending_anchor') && !inclusionValid && inclusionReason) {
232
487
  errors.push(inclusionReason);
488
+ }
489
+ let epochReason;
490
+ if (input.head?.epoch == null) {
491
+ const link = (0, epoch_js_1.verifyEpochLink)(input.head, null);
492
+ if (!link.ok)
493
+ epochReason = link.reason || 'epoch_missing';
494
+ }
495
+ const needsEpoch = input.head?.epoch != null || input.epochRecord != null;
496
+ if (!epochReason && needsEpoch) {
497
+ if (!input.epochRecord)
498
+ epochReason = 'epoch_record_missing';
499
+ else {
500
+ const checked = (0, epoch_js_1.verifyEpochRecord)(input.epochRecord, { verifySignature: input.verifyEpochSignature });
501
+ if (!checked.ok)
502
+ epochReason = checked.reason || 'epoch_record';
503
+ else if (input.head?.epoch != null && Number(input.head.epoch) > 1) {
504
+ const prev = (input.epochRecord.epochs || []).find((row) => Number(row.epoch) === Number(input.head.epoch) - 1);
505
+ const link = (0, epoch_js_1.verifyEpochLink)(input.head, prev ? {
506
+ root: prev.final_root || prev.opening_root || null,
507
+ tree_size: prev.final_size ?? prev.opening_size ?? null,
508
+ } : null);
509
+ if (!link.ok)
510
+ epochReason = link.reason || 'epoch_link';
511
+ }
512
+ else if (input.head) {
513
+ const link = (0, epoch_js_1.verifyEpochLink)(input.head, null);
514
+ if (!link.ok)
515
+ epochReason = link.reason || 'epoch_link';
516
+ }
517
+ }
518
+ }
519
+ if (epochReason)
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');
233
550
  const solanaRpc = input.solanaRpcUrl || process.env.SOLANA_RPC_URL || solana_payer_js_1.SOLANA_RPC_URL;
234
551
  const baseRpc = input.baseRpcUrl || base_payer_js_1.BASE_RPC_URL;
235
552
  const signature = root && inclusionValid ? solanaSignature(input.head) : null;
@@ -241,6 +558,7 @@ async function verifyAnchoredRoot(input) {
241
558
  slot: input.head?.anchors?.solana?.slot ?? null,
242
559
  cluster: input.head?.anchors?.solana?.cluster ?? null,
243
560
  memo: input.head?.anchors?.solana?.memo ?? null,
561
+ fee_payer: input.head?.anchors?.solana?.fee_payer ?? null,
244
562
  reason: undefined,
245
563
  };
246
564
  const base = {
@@ -248,17 +566,53 @@ async function verifyAnchoredRoot(input) {
248
566
  valid: false,
249
567
  tx: input.head?.anchors?.base?.tx || input.head?.anchor?.tx || input.head?.anchor_tx || null,
250
568
  chain_id: input.head?.anchors?.base?.chain_id ?? input.head?.anchor?.chain_id ?? null,
569
+ from: input.head?.anchors?.base?.from ?? null,
251
570
  reason: undefined,
252
571
  };
253
- if (!inclusionValid) {
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) {
254
593
  solana.reason = 'inclusion_failed';
255
594
  base.reason = 'inclusion_failed';
256
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
+ }
257
607
  else {
258
608
  if (!signature) {
259
609
  solana.reason = 'pending';
260
610
  errors.push('solana_pending');
261
611
  }
612
+ else if (!signedPayer) {
613
+ solana.reason = 'fee_payer_missing';
614
+ errors.push('fee_payer_missing');
615
+ }
262
616
  else {
263
617
  solana.checked = true;
264
618
  try {
@@ -295,6 +649,10 @@ async function verifyAnchoredRoot(input) {
295
649
  if (!solana.reason && headSlot != null && tx.slot != null && Number(tx.slot) !== Number(headSlot)) {
296
650
  solana.reason = 'slot_mismatch';
297
651
  }
652
+ const payer = solanaFeePayer(tx);
653
+ if (!solana.reason && payer !== signedPayer) {
654
+ solana.reason = 'fee_payer_mismatch';
655
+ }
298
656
  if (!solana.reason) {
299
657
  solana.valid = true;
300
658
  solana.memo = matched[0];
@@ -313,6 +671,10 @@ async function verifyAnchoredRoot(input) {
313
671
  base.reason = 'pending';
314
672
  errors.push('base_pending');
315
673
  }
674
+ else if (!signedFrom) {
675
+ base.reason = 'anchor_sender_missing';
676
+ errors.push('anchor_sender_missing');
677
+ }
316
678
  else {
317
679
  base.checked = true;
318
680
  try {
@@ -330,6 +692,8 @@ async function verifyAnchoredRoot(input) {
330
692
  const inputData = String(tx.input || '').toLowerCase().replace(/^0x/, '');
331
693
  if (inputData !== root)
332
694
  base.reason = 'calldata_mismatch';
695
+ else if (!tx.from || !(0, anchor_trust_js_1.sameBaseAddress)(tx.from, signedFrom))
696
+ base.reason = 'sender_mismatch';
333
697
  else {
334
698
  base.valid = true;
335
699
  base.tx = tx.hash || txHash;
@@ -344,22 +708,111 @@ async function verifyAnchoredRoot(input) {
344
708
  errors.push(`base:${base.reason}`);
345
709
  }
346
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
+ }
347
745
  let overall;
348
- if (!inclusionValid || (solana.checked && !solana.valid) || (base.checked && !base.valid))
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)
349
771
  overall = 'failed';
350
772
  else if (solana.valid && base.valid)
351
773
  overall = 'verified';
352
774
  else
353
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
+ }
354
782
  return {
355
783
  overall,
356
784
  root,
357
- inclusion: { valid: inclusionValid, leaf: leafHex, leaf_source: leafSource, reason: inclusionReason },
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
+ },
358
794
  solana,
359
795
  base,
360
- proves: exports.ANCHOR_PROVES,
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,
361
813
  does_not_prove: doesNotProve,
362
814
  errors,
815
+ issuer_pin: issuerPin,
363
816
  };
364
817
  }
365
818
  //# sourceMappingURL=anchor-witness.js.map