@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.
- package/README.md +37 -16
- 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 +134 -7
- package/dist/anchor-witness.d.ts.map +1 -1
- package/dist/anchor-witness.js +497 -44
- 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 +674 -30
- package/dist/cli.js.map +1 -1
- package/dist/epoch.d.ts +143 -0
- package/dist/epoch.d.ts.map +1 -0
- package/dist/epoch.js +352 -0
- package/dist/epoch.js.map +1 -0
- package/dist/index.d.ts +86 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +395 -118
- 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,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
|
-
/**
|
|
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': '
|
|
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
|
|
34
|
-
'The
|
|
35
|
-
'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.',
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
94
|
+
return null;
|
|
64
95
|
const sib = Buffer.from(step.hash, 'hex');
|
|
65
|
-
const
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|