@xfuel/verify 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -17
- package/dist/anchor-witness.d.ts +138 -0
- package/dist/anchor-witness.d.ts.map +1 -0
- package/dist/anchor-witness.js +365 -0
- package/dist/anchor-witness.js.map +1 -0
- package/dist/base-payer.d.ts +8 -1
- package/dist/base-payer.d.ts.map +1 -1
- package/dist/base-payer.js +64 -20
- package/dist/base-payer.js.map +1 -1
- package/dist/canonical-preimage.d.ts +9 -0
- package/dist/canonical-preimage.d.ts.map +1 -0
- package/dist/canonical-preimage.js +25 -0
- package/dist/canonical-preimage.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +424 -25
- package/dist/cli.js.map +1 -1
- package/dist/head-binding.d.ts +24 -0
- package/dist/head-binding.d.ts.map +1 -0
- package/dist/head-binding.js +61 -0
- package/dist/head-binding.js.map +1 -0
- package/dist/index.d.ts +263 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +919 -140
- package/dist/index.js.map +1 -1
- package/dist/issuer-history.d.ts +91 -0
- package/dist/issuer-history.d.ts.map +1 -0
- package/dist/issuer-history.js +292 -0
- package/dist/issuer-history.js.map +1 -0
- package/dist/jcs.d.ts +8 -0
- package/dist/jcs.d.ts.map +1 -0
- package/dist/jcs.js +64 -0
- package/dist/jcs.js.map +1 -0
- package/dist/jws.d.ts +23 -0
- package/dist/jws.d.ts.map +1 -1
- package/dist/jws.js +71 -1
- package/dist/jws.js.map +1 -1
- package/dist/payer.d.ts +16 -5
- package/dist/payer.d.ts.map +1 -1
- package/dist/payer.js +38 -26
- package/dist/payer.js.map +1 -1
- package/dist/preimage.d.ts +49 -0
- package/dist/preimage.d.ts.map +1 -0
- package/dist/preimage.js +203 -0
- package/dist/preimage.js.map +1 -0
- package/dist/receipt-lane.d.ts +234 -0
- package/dist/receipt-lane.d.ts.map +1 -0
- package/dist/receipt-lane.js +443 -0
- package/dist/receipt-lane.js.map +1 -0
- package/dist/refusal.d.ts +90 -0
- package/dist/refusal.d.ts.map +1 -0
- package/dist/refusal.js +226 -0
- package/dist/refusal.js.map +1 -0
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ console.log(result.matches); // true if binding verified
|
|
|
25
25
|
// Full verification including on-chain nullifier check
|
|
26
26
|
const fullResult = await verifyReceipt(receipt, { checkNullifier: true });
|
|
27
27
|
console.log(fullResult.overall); // 'verified' | 'partial' | 'failed'
|
|
28
|
+
console.log(fullResult.receipt_lane.freeze); // unsigned; does not change overall
|
|
28
29
|
```
|
|
29
30
|
|
|
30
31
|
### CLI
|
|
@@ -33,6 +34,15 @@ console.log(fullResult.overall); // 'verified' | 'partial' | 'failed'
|
|
|
33
34
|
# Local binding verification (no network required)
|
|
34
35
|
npx xfuel-verify receipt.json
|
|
35
36
|
|
|
37
|
+
# Hash the canonical object (must match signed payload_hash) and read not_after
|
|
38
|
+
# from a pinned issuer-history snapshot. Both flags are offline.
|
|
39
|
+
npx xfuel-verify receipt.json --canonical-preimage preimage.json --issuer-history-file issuer-history.json
|
|
40
|
+
|
|
41
|
+
# A signed refusal (schema chit402.refusal.v1) is not a payment.
|
|
42
|
+
# The check proves the issuer refused, at the signed anchor, for that code.
|
|
43
|
+
# It does not prove a payment or that the block still stands.
|
|
44
|
+
npx xfuel-verify refusal.json
|
|
45
|
+
|
|
36
46
|
# With on-chain nullifier check (requires network)
|
|
37
47
|
npx xfuel-verify receipt.json --check-nullifier
|
|
38
48
|
|
|
@@ -40,7 +50,10 @@ npx xfuel-verify receipt.json --check-nullifier
|
|
|
40
50
|
npx xfuel-verify receipt.json --json
|
|
41
51
|
|
|
42
52
|
# From stdin
|
|
43
|
-
curl -s https://api.
|
|
53
|
+
curl -s https://api.chit402.com/receipt/task-123?format=json | npx xfuel-verify -
|
|
54
|
+
|
|
55
|
+
# Dual anchor: inclusion, Solana memo, Base calldata
|
|
56
|
+
npx xfuel-verify receipt.json inclusion.json head.json --rpc
|
|
44
57
|
```
|
|
45
58
|
|
|
46
59
|
## What This Verifies
|
|
@@ -48,42 +61,70 @@ curl -s https://api.xfuel.app/receipt/task-123?format=json | npx xfuel-verify -
|
|
|
48
61
|
| Check | Requires Network? | Description |
|
|
49
62
|
|-------|-------------------|-------------|
|
|
50
63
|
| Payment binding | No | Recompute commitment from receipt fields |
|
|
51
|
-
| Issuer signature | No | ES256
|
|
64
|
+
| Issuer signature | No, unless `--fetch-jwks` | ES256. Trusted only via JWKS (by kid) or a pinned RFC 7638 kid |
|
|
52
65
|
| Output hash | No | Hash is on the receipt |
|
|
53
66
|
| On-chain settlement | Yes | Query Base RPC for tx |
|
|
54
67
|
| Nullifier anchor | Yes | Query ZKVerifierSP1 contract |
|
|
68
|
+
| Anchored receipt root | Yes, with `--rpc` | Inclusion proof, then Solana memo and Base calldata for that root |
|
|
69
|
+
| Canonical preimage | No | SHA-256 of `--canonical-preimage`, or the stored canonical object, matches signed `payload_hash` |
|
|
70
|
+
| Issuer history | No, with `--issuer-history-file` | `iat` is inside the kid's `not_before` / `not_after`. A payload v10 pin must match the snapshot hash |
|
|
55
71
|
|
|
56
72
|
## Issuer Signature Verification (ES256)
|
|
57
73
|
|
|
58
|
-
Receipts include
|
|
74
|
+
Receipts include `issuer_signature.jws` (compact ES256 / P-256). The signature
|
|
75
|
+
counts only when the verifying key is trusted:
|
|
59
76
|
|
|
60
|
-
|
|
77
|
+
1. **JWKS by kid** — a key in `--jwks-file`, `--jwks-url`, or `--fetch-jwks`
|
|
78
|
+
(allowlisted host, default `api.chit402.com`) whose `kid` matches the JWS.
|
|
79
|
+
2. **Pinned thumbprint** — the embedded `issuer_jwk` is used only when its
|
|
80
|
+
RFC 7638 thumbprint equals a trusted kid. The default offline pin is the
|
|
81
|
+
production kid `IvFpmC-vPhkY_v0vidsrWVT9uzlE5XWKZgAEOeJTq1Q`. Override with
|
|
82
|
+
`--trusted-kid`, or disable with `--no-trusted-kid`.
|
|
61
83
|
|
|
62
|
-
|
|
84
|
+
`issuer_jwk` on the receipt is not a trust root. A copy re-signed with an
|
|
85
|
+
arbitrary P-256 key reports `key untrusted` (`issuer_signature.valid === false`),
|
|
86
|
+
including when `--jwks-file` points at the real JWKS.
|
|
63
87
|
|
|
64
88
|
```bash
|
|
65
|
-
# Offline
|
|
89
|
+
# Offline: default production pin, no network
|
|
66
90
|
npx xfuel-verify receipt.json
|
|
91
|
+
|
|
92
|
+
# Recompute published preimages and check the kid window.
|
|
93
|
+
# A payload v10 receipt pins issuer_history. Hash the stored canonical object:
|
|
94
|
+
curl -sS "https://api.chit402.com/receipt/RECEIPT_ID?format=json" -o receipt.json
|
|
95
|
+
curl -sS "https://api.chit402.com/receipt/RECEIPT_ID/preimage" -o preimage.json
|
|
96
|
+
curl -sS "https://api.chit402.com/.well-known/issuer-history.json?version=1" -o issuer-history.json
|
|
97
|
+
npx xfuel-verify receipt.json --canonical-preimage preimage.json --issuer-history-file issuer-history.json
|
|
98
|
+
|
|
99
|
+
# Trust the published JWKS instead of (or in addition to) the pin
|
|
100
|
+
curl -o issuer-jwks.json https://api.chit402.com/.well-known/jwks.json
|
|
101
|
+
npx xfuel-verify receipt.json --jwks-file issuer-jwks.json --no-trusted-kid
|
|
102
|
+
|
|
103
|
+
# Fetch the issuer JWKS (https://api.chit402.com/.well-known/jwks.json)
|
|
104
|
+
npx xfuel-verify receipt.json --fetch-jwks
|
|
67
105
|
```
|
|
68
106
|
|
|
69
107
|
```typescript
|
|
70
108
|
import { verifyReceipt } from '@xfuel/verify';
|
|
71
109
|
|
|
72
|
-
const result = await verifyReceipt(receipt);
|
|
73
|
-
console.log(result.issuer_signature.valid);
|
|
110
|
+
const result = await verifyReceipt(receipt);
|
|
111
|
+
console.log(result.issuer_signature.valid); // true only for a trusted key
|
|
112
|
+
console.log(result.issuer_signature.key_trusted);
|
|
113
|
+
console.log(result.amount_usdc); // from verified claims, not the outer copy
|
|
114
|
+
console.log(result.claim_mismatches); // outer payment / caller_binding vs JWS
|
|
74
115
|
```
|
|
75
116
|
|
|
76
|
-
|
|
117
|
+
Amount, payer, payee, asset, and tx are read from the verified JWS claims.
|
|
118
|
+
If the unsigned outer `payment` or `caller_binding` disagrees, verification
|
|
119
|
+
fails. `--check-payer` on Base confirms payer, payee, asset, and amount in the
|
|
120
|
+
USDC `Transfer` log.
|
|
77
121
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
curl -o issuer-jwks.json https://api.chit402.com/.well-known/jwks.json
|
|
81
|
-
|
|
82
|
-
# Verify receipt with JWKS file (no network during verification)
|
|
83
|
-
npx xfuel-verify receipt.json --jwks-file issuer-jwks.json
|
|
84
|
-
```
|
|
122
|
+
A paid USDC receipt whose signed `binding.expected_commitment` is null is
|
|
123
|
+
reported as “No payment-binding commitment”, not as an unmetered or TFUEL receipt.
|
|
85
124
|
|
|
86
|
-
The CLI does **not**
|
|
125
|
+
The CLI does **not** fetch JWKS unless `--fetch-jwks` or `--jwks-url` is set.
|
|
126
|
+
Exit code 1 is returned when the key is untrusted, the signature is invalid, or
|
|
127
|
+
a signed claim disagrees with the outer copy.
|
|
87
128
|
|
|
88
129
|
## Frozen Fields
|
|
89
130
|
|
|
@@ -181,6 +222,10 @@ Full verification combining binding and optional nullifier check.
|
|
|
181
222
|
}
|
|
182
223
|
```
|
|
183
224
|
|
|
225
|
+
## Build digest
|
|
226
|
+
|
|
227
|
+
`npm run digest` writes `BUILD_DIGEST.txt`. That file is the SHA-256 of the sorted `src/**/*.ts` listing, not a bit-reproducible `tsc` binary. The same sources produce the same digest. The receipt-tree genesis leaf copies this digest so a holder can see which verifier source the issuer vouches for. It does not prove the process that served the receipt ran that exact binary.
|
|
228
|
+
|
|
184
229
|
## License
|
|
185
230
|
|
|
186
231
|
Apache-2.0
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export declare const MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
2
|
+
export declare const MEMO_PROGRAM_ID_V1 = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo";
|
|
3
|
+
/** Genesis hashes from the Solana cluster CAIP-2 references. */
|
|
4
|
+
export declare const SOLANA_GENESIS: Record<string, string>;
|
|
5
|
+
export declare const BASE_MAINNET_CHAIN_ID = 8453;
|
|
6
|
+
export declare const ANCHOR_PROVES: string[];
|
|
7
|
+
export declare const ANCHOR_DOES_NOT_PROVE: string[];
|
|
8
|
+
export interface InclusionStep {
|
|
9
|
+
hash: string;
|
|
10
|
+
position: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AnchorReceipt {
|
|
13
|
+
task_id?: string;
|
|
14
|
+
row_hash?: string | null;
|
|
15
|
+
book_chain?: {
|
|
16
|
+
row_hash?: string | null;
|
|
17
|
+
} | null;
|
|
18
|
+
}
|
|
19
|
+
export interface AnchorInclusion {
|
|
20
|
+
task_id?: string;
|
|
21
|
+
leaf_index: number;
|
|
22
|
+
tree_size: number;
|
|
23
|
+
root: string;
|
|
24
|
+
leaf?: string;
|
|
25
|
+
row_hash?: string | null;
|
|
26
|
+
proof: InclusionStep[];
|
|
27
|
+
}
|
|
28
|
+
export interface AnchorHead {
|
|
29
|
+
root: string;
|
|
30
|
+
tree_size?: number;
|
|
31
|
+
anchor_tx?: string | null;
|
|
32
|
+
anchor?: {
|
|
33
|
+
tx?: string | null;
|
|
34
|
+
calldata?: string | null;
|
|
35
|
+
chain_id?: number | null;
|
|
36
|
+
} | null;
|
|
37
|
+
anchors?: {
|
|
38
|
+
base?: {
|
|
39
|
+
tx?: string | null;
|
|
40
|
+
calldata?: string | null;
|
|
41
|
+
chain_id?: number | null;
|
|
42
|
+
status?: string;
|
|
43
|
+
} | null;
|
|
44
|
+
solana?: {
|
|
45
|
+
signature?: string | null;
|
|
46
|
+
slot?: number | null;
|
|
47
|
+
cluster?: string | null;
|
|
48
|
+
memo?: string | null;
|
|
49
|
+
status?: string | null;
|
|
50
|
+
} | null;
|
|
51
|
+
} | null;
|
|
52
|
+
}
|
|
53
|
+
interface SolanaIx {
|
|
54
|
+
program?: string;
|
|
55
|
+
programId?: string;
|
|
56
|
+
parsed?: string | {
|
|
57
|
+
memo?: string;
|
|
58
|
+
info?: string;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export interface SolanaAnchorTx {
|
|
62
|
+
slot?: number;
|
|
63
|
+
meta?: {
|
|
64
|
+
err?: unknown;
|
|
65
|
+
logMessages?: string[];
|
|
66
|
+
innerInstructions?: Array<{
|
|
67
|
+
instructions?: SolanaIx[];
|
|
68
|
+
}>;
|
|
69
|
+
} | null;
|
|
70
|
+
transaction?: {
|
|
71
|
+
message?: {
|
|
72
|
+
instructions?: SolanaIx[];
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export interface BaseAnchorTx {
|
|
77
|
+
hash?: string;
|
|
78
|
+
input?: string;
|
|
79
|
+
chainId?: number;
|
|
80
|
+
}
|
|
81
|
+
export interface AnchorWitnessResult {
|
|
82
|
+
overall: 'verified' | 'partial' | 'failed';
|
|
83
|
+
root: string | null;
|
|
84
|
+
inclusion: {
|
|
85
|
+
valid: boolean;
|
|
86
|
+
leaf: string | null;
|
|
87
|
+
leaf_source: string;
|
|
88
|
+
reason?: string;
|
|
89
|
+
};
|
|
90
|
+
solana: {
|
|
91
|
+
checked: boolean;
|
|
92
|
+
valid: boolean;
|
|
93
|
+
signature: string | null;
|
|
94
|
+
slot: number | null;
|
|
95
|
+
cluster: string | null;
|
|
96
|
+
memo: string | null;
|
|
97
|
+
reason?: string;
|
|
98
|
+
};
|
|
99
|
+
base: {
|
|
100
|
+
checked: boolean;
|
|
101
|
+
valid: boolean;
|
|
102
|
+
tx: string | null;
|
|
103
|
+
chain_id: number | null;
|
|
104
|
+
reason?: string;
|
|
105
|
+
};
|
|
106
|
+
proves: string[];
|
|
107
|
+
does_not_prove: string[];
|
|
108
|
+
errors: string[];
|
|
109
|
+
}
|
|
110
|
+
export declare function leafHash(bytes: Uint8Array): Buffer;
|
|
111
|
+
export declare function normalizeRoot(root: string | null | undefined): string | null;
|
|
112
|
+
/** RFC 6962-style inclusion. Same rule as the gateway tree. */
|
|
113
|
+
export declare function verifyMerkleInclusion(leaf: Buffer, index: number, treeSize: number, rootHex: string, proof: InclusionStep[]): boolean;
|
|
114
|
+
export declare function parseAnchorMemo(memo: string): {
|
|
115
|
+
scope: string;
|
|
116
|
+
day: string;
|
|
117
|
+
root: string;
|
|
118
|
+
prev: string;
|
|
119
|
+
} | null;
|
|
120
|
+
export declare function extractMemos(tx: SolanaAnchorTx | null | undefined): string[];
|
|
121
|
+
export declare function fetchSolanaGenesisHash(rpcUrl: string): Promise<string>;
|
|
122
|
+
export declare function fetchBaseAnchorTransaction(txHash: string, rpcUrl: string): Promise<BaseAnchorTx | null>;
|
|
123
|
+
export interface VerifyAnchoredRootInput {
|
|
124
|
+
receipt: AnchorReceipt;
|
|
125
|
+
inclusion: AnchorInclusion;
|
|
126
|
+
head: AnchorHead;
|
|
127
|
+
baseRpcUrl?: string;
|
|
128
|
+
solanaRpcUrl?: string;
|
|
129
|
+
fetchSolanaTx?: (signature: string, rpcUrl: string) => Promise<SolanaAnchorTx | null>;
|
|
130
|
+
fetchGenesis?: (rpcUrl: string) => Promise<string>;
|
|
131
|
+
fetchBaseTx?: (txHash: string, rpcUrl: string) => Promise<BaseAnchorTx | null>;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Verify inclusion, then the Solana memo and the Base calldata for that root.
|
|
135
|
+
*/
|
|
136
|
+
export declare function verifyAnchoredRoot(input: VerifyAnchoredRootInput): Promise<AnchorWitnessResult>;
|
|
137
|
+
export {};
|
|
138
|
+
//# sourceMappingURL=anchor-witness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anchor-witness.d.ts","sourceRoot":"","sources":["../src/anchor-witness.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"use strict";
|
|
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;
|
|
4
|
+
exports.leafHash = leafHash;
|
|
5
|
+
exports.normalizeRoot = normalizeRoot;
|
|
6
|
+
exports.verifyMerkleInclusion = verifyMerkleInclusion;
|
|
7
|
+
exports.parseAnchorMemo = parseAnchorMemo;
|
|
8
|
+
exports.extractMemos = extractMemos;
|
|
9
|
+
exports.fetchSolanaGenesisHash = fetchSolanaGenesisHash;
|
|
10
|
+
exports.fetchBaseAnchorTransaction = fetchBaseAnchorTransaction;
|
|
11
|
+
exports.verifyAnchoredRoot = verifyAnchoredRoot;
|
|
12
|
+
/**
|
|
13
|
+
* Check a receipt leaf against a signed tree head, then against the two
|
|
14
|
+
* anchors that publish that root: an SPL Memo on Solana and calldata on Base.
|
|
15
|
+
*
|
|
16
|
+
* Inclusion is local. The chain checks use the RPC you pass. A matching memo
|
|
17
|
+
* and matching calldata show the issuer published this root. They do not show
|
|
18
|
+
* the payment.
|
|
19
|
+
*/
|
|
20
|
+
const node_crypto_1 = require("node:crypto");
|
|
21
|
+
const base_payer_js_1 = require("./base-payer.js");
|
|
22
|
+
const solana_payer_js_1 = require("./solana-payer.js");
|
|
23
|
+
exports.MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
|
|
24
|
+
exports.MEMO_PROGRAM_ID_V1 = 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo';
|
|
25
|
+
/** Genesis hashes from the Solana cluster CAIP-2 references. */
|
|
26
|
+
exports.SOLANA_GENESIS = {
|
|
27
|
+
'mainnet-beta': '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
|
|
28
|
+
devnet: 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG',
|
|
29
|
+
testnet: '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY',
|
|
30
|
+
};
|
|
31
|
+
exports.BASE_MAINNET_CHAIN_ID = 8453;
|
|
32
|
+
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.',
|
|
36
|
+
];
|
|
37
|
+
exports.ANCHOR_DOES_NOT_PROVE = [
|
|
38
|
+
'It does not prove the payment, the payer, the payee, or the model output.',
|
|
39
|
+
'It does not prove a receipt appended after this tree size is inside the root.',
|
|
40
|
+
'It does not prove the issuer included only honest rows. It proves this leaf hashes into the published root.',
|
|
41
|
+
'It does not prove the RPC itself is honest. It checks the transaction and genesis hash that RPC returned.',
|
|
42
|
+
];
|
|
43
|
+
function sha256(buf) {
|
|
44
|
+
return (0, node_crypto_1.createHash)('sha256').update(buf).digest();
|
|
45
|
+
}
|
|
46
|
+
function leafHash(bytes) {
|
|
47
|
+
return sha256(Buffer.concat([Buffer.from([0x00]), Buffer.from(bytes)]));
|
|
48
|
+
}
|
|
49
|
+
function nodeHash(left, right) {
|
|
50
|
+
return sha256(Buffer.concat([Buffer.from([0x01]), left, right]));
|
|
51
|
+
}
|
|
52
|
+
function normalizeRoot(root) {
|
|
53
|
+
const hex = String(root || '').replace(/^0x/, '').toLowerCase();
|
|
54
|
+
return /^[0-9a-f]{64}$/.test(hex) ? hex : null;
|
|
55
|
+
}
|
|
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);
|
|
61
|
+
for (const step of proof) {
|
|
62
|
+
if (!step || !/^[0-9a-fA-F]{64}$/.test(step.hash))
|
|
63
|
+
return false;
|
|
64
|
+
const sib = Buffer.from(step.hash, 'hex');
|
|
65
|
+
const next = step.position === 'left' ? nodeHash(sib, hash) : nodeHash(hash, sib);
|
|
66
|
+
hash = new Uint8Array(next);
|
|
67
|
+
}
|
|
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')
|
|
75
|
+
return null;
|
|
76
|
+
const scope = parts[3];
|
|
77
|
+
const day = parts[4];
|
|
78
|
+
const root = parts[5];
|
|
79
|
+
const prev = parts[6];
|
|
80
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(scope))
|
|
81
|
+
return null;
|
|
82
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(day))
|
|
83
|
+
return null;
|
|
84
|
+
if (!/^[0-9a-f]{64}$/.test(root) || !/^[0-9a-f]{64}$/.test(prev))
|
|
85
|
+
return null;
|
|
86
|
+
return { scope, day, root, prev };
|
|
87
|
+
}
|
|
88
|
+
function isMemoIx(ix) {
|
|
89
|
+
const program = ix.program || '';
|
|
90
|
+
const programId = ix.programId || '';
|
|
91
|
+
return program === 'spl-memo'
|
|
92
|
+
|| programId === exports.MEMO_PROGRAM_ID
|
|
93
|
+
|| programId === exports.MEMO_PROGRAM_ID_V1;
|
|
94
|
+
}
|
|
95
|
+
function extractMemos(tx) {
|
|
96
|
+
if (!tx)
|
|
97
|
+
return [];
|
|
98
|
+
const found = [];
|
|
99
|
+
const push = (value) => {
|
|
100
|
+
if (value && !found.includes(value))
|
|
101
|
+
found.push(value);
|
|
102
|
+
};
|
|
103
|
+
const visit = (ix) => {
|
|
104
|
+
if (!ix || !isMemoIx(ix))
|
|
105
|
+
return;
|
|
106
|
+
if (typeof ix.parsed === 'string')
|
|
107
|
+
push(ix.parsed);
|
|
108
|
+
else if (ix.parsed && typeof ix.parsed.memo === 'string')
|
|
109
|
+
push(ix.parsed.memo);
|
|
110
|
+
else if (ix.parsed && typeof ix.parsed.info === 'string')
|
|
111
|
+
push(ix.parsed.info);
|
|
112
|
+
};
|
|
113
|
+
for (const ix of tx.transaction?.message?.instructions || [])
|
|
114
|
+
visit(ix);
|
|
115
|
+
for (const inner of tx.meta?.innerInstructions || []) {
|
|
116
|
+
for (const ix of inner.instructions || [])
|
|
117
|
+
visit(ix);
|
|
118
|
+
}
|
|
119
|
+
for (const line of tx.meta?.logMessages || []) {
|
|
120
|
+
const match = /Memo \(len \d+\): "(.*)"/.exec(line);
|
|
121
|
+
if (match)
|
|
122
|
+
push(match[1]);
|
|
123
|
+
}
|
|
124
|
+
return found;
|
|
125
|
+
}
|
|
126
|
+
async function defaultRpc(url, method, params) {
|
|
127
|
+
const res = await fetch(url, {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: { 'content-type': 'application/json' },
|
|
130
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
|
|
131
|
+
});
|
|
132
|
+
if (!res.ok)
|
|
133
|
+
throw new Error(`HTTP ${res.status}`);
|
|
134
|
+
const json = await res.json();
|
|
135
|
+
if (json.error)
|
|
136
|
+
throw new Error(json.error.message || 'rpc_error');
|
|
137
|
+
return json.result ?? null;
|
|
138
|
+
}
|
|
139
|
+
async function fetchSolanaGenesisHash(rpcUrl) {
|
|
140
|
+
const result = await defaultRpc(rpcUrl, 'getGenesisHash', []);
|
|
141
|
+
if (typeof result !== 'string' || result.length === 0)
|
|
142
|
+
throw new Error('no_genesis');
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
async function fetchBaseAnchorTransaction(txHash, rpcUrl) {
|
|
146
|
+
const tx = await defaultRpc(rpcUrl, 'eth_getTransactionByHash', [txHash]);
|
|
147
|
+
const chainHex = await defaultRpc(rpcUrl, 'eth_chainId', []);
|
|
148
|
+
if (!tx)
|
|
149
|
+
return null;
|
|
150
|
+
return {
|
|
151
|
+
hash: tx.hash,
|
|
152
|
+
input: tx.input,
|
|
153
|
+
chainId: chainHex ? Number(chainHex) : undefined,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
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' };
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
function solanaSignature(head) {
|
|
166
|
+
const sol = head.anchors?.solana;
|
|
167
|
+
if (!sol || sol.status === 'pending')
|
|
168
|
+
return null;
|
|
169
|
+
return sol.signature || null;
|
|
170
|
+
}
|
|
171
|
+
function baseTxHash(head) {
|
|
172
|
+
const base = head.anchors?.base;
|
|
173
|
+
if (base?.status === 'pending')
|
|
174
|
+
return null;
|
|
175
|
+
return base?.tx || head.anchor?.tx || head.anchor_tx || null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Verify inclusion, then the Solana memo and the Base calldata for that root.
|
|
179
|
+
*/
|
|
180
|
+
async function verifyAnchoredRoot(input) {
|
|
181
|
+
const errors = [];
|
|
182
|
+
const doesNotProve = [...exports.ANCHOR_DOES_NOT_PROVE];
|
|
183
|
+
const root = normalizeRoot(input.head?.root);
|
|
184
|
+
const inclusionRoot = normalizeRoot(input.inclusion?.root);
|
|
185
|
+
let leafHex = null;
|
|
186
|
+
let leafSource = 'none';
|
|
187
|
+
let inclusionValid = false;
|
|
188
|
+
let inclusionReason;
|
|
189
|
+
const taskId = input.receipt?.task_id ? String(input.receipt.task_id) : '';
|
|
190
|
+
if (!taskId) {
|
|
191
|
+
inclusionReason = 'missing_task_id';
|
|
192
|
+
}
|
|
193
|
+
else if (input.inclusion?.task_id && String(input.inclusion.task_id) !== taskId) {
|
|
194
|
+
inclusionReason = 'task_mismatch';
|
|
195
|
+
}
|
|
196
|
+
else if (!root || !inclusionRoot) {
|
|
197
|
+
inclusionReason = 'bad_root';
|
|
198
|
+
}
|
|
199
|
+
else if (root !== inclusionRoot) {
|
|
200
|
+
inclusionReason = 'root_mismatch';
|
|
201
|
+
}
|
|
202
|
+
else if (input.head.tree_size != null && Number(input.head.tree_size) !== Number(input.inclusion.tree_size)) {
|
|
203
|
+
inclusionReason = 'tree_size_mismatch';
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
const row = rowHashFrom(input.receipt, input.inclusion);
|
|
207
|
+
let leaf = null;
|
|
208
|
+
if (row) {
|
|
209
|
+
leaf = leafHash(Buffer.from(`${taskId}|${row.rowHash}`));
|
|
210
|
+
leafSource = row.source;
|
|
211
|
+
if (input.inclusion.leaf && input.inclusion.leaf.toLowerCase() !== leaf.toString('hex')) {
|
|
212
|
+
inclusionReason = 'leaf_mismatch';
|
|
213
|
+
leaf = null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
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
|
+
else {
|
|
222
|
+
inclusionReason = 'no_leaf';
|
|
223
|
+
}
|
|
224
|
+
if (leaf) {
|
|
225
|
+
leafHex = leaf.toString('hex');
|
|
226
|
+
inclusionValid = verifyMerkleInclusion(leaf, Number(input.inclusion.leaf_index), Number(input.inclusion.tree_size), root, input.inclusion.proof || []);
|
|
227
|
+
if (!inclusionValid)
|
|
228
|
+
inclusionReason = 'inclusion_failed';
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!inclusionValid && inclusionReason)
|
|
232
|
+
errors.push(inclusionReason);
|
|
233
|
+
const solanaRpc = input.solanaRpcUrl || process.env.SOLANA_RPC_URL || solana_payer_js_1.SOLANA_RPC_URL;
|
|
234
|
+
const baseRpc = input.baseRpcUrl || base_payer_js_1.BASE_RPC_URL;
|
|
235
|
+
const signature = root && inclusionValid ? solanaSignature(input.head) : null;
|
|
236
|
+
const txHash = root && inclusionValid ? baseTxHash(input.head) : null;
|
|
237
|
+
const solana = {
|
|
238
|
+
checked: false,
|
|
239
|
+
valid: false,
|
|
240
|
+
signature: input.head?.anchors?.solana?.signature ?? null,
|
|
241
|
+
slot: input.head?.anchors?.solana?.slot ?? null,
|
|
242
|
+
cluster: input.head?.anchors?.solana?.cluster ?? null,
|
|
243
|
+
memo: input.head?.anchors?.solana?.memo ?? null,
|
|
244
|
+
reason: undefined,
|
|
245
|
+
};
|
|
246
|
+
const base = {
|
|
247
|
+
checked: false,
|
|
248
|
+
valid: false,
|
|
249
|
+
tx: input.head?.anchors?.base?.tx || input.head?.anchor?.tx || input.head?.anchor_tx || null,
|
|
250
|
+
chain_id: input.head?.anchors?.base?.chain_id ?? input.head?.anchor?.chain_id ?? null,
|
|
251
|
+
reason: undefined,
|
|
252
|
+
};
|
|
253
|
+
if (!inclusionValid) {
|
|
254
|
+
solana.reason = 'inclusion_failed';
|
|
255
|
+
base.reason = 'inclusion_failed';
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
if (!signature) {
|
|
259
|
+
solana.reason = 'pending';
|
|
260
|
+
errors.push('solana_pending');
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
solana.checked = true;
|
|
264
|
+
try {
|
|
265
|
+
const fetchTx = input.fetchSolanaTx || ((sig, url) => (0, solana_payer_js_1.fetchSolanaTransaction)(sig, url));
|
|
266
|
+
const fetchGenesis = input.fetchGenesis || fetchSolanaGenesisHash;
|
|
267
|
+
const tx = await fetchTx(signature, solanaRpc);
|
|
268
|
+
if (!tx) {
|
|
269
|
+
solana.reason = 'tx_not_found';
|
|
270
|
+
}
|
|
271
|
+
else if (tx.meta?.err) {
|
|
272
|
+
solana.reason = 'tx_failed';
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
const memos = extractMemos(tx);
|
|
276
|
+
const recorded = input.head.anchors?.solana?.memo || null;
|
|
277
|
+
const matched = memos.filter((memo) => {
|
|
278
|
+
const parsed = parseAnchorMemo(memo);
|
|
279
|
+
return Boolean(parsed && parsed.root === root && memo.includes(root) && (!recorded || memo === recorded));
|
|
280
|
+
});
|
|
281
|
+
const cluster = input.head.anchors?.solana?.cluster || null;
|
|
282
|
+
const expectedGenesis = cluster ? exports.SOLANA_GENESIS[cluster] : null;
|
|
283
|
+
if (matched.length === 0)
|
|
284
|
+
solana.reason = 'memo_mismatch';
|
|
285
|
+
else if (!cluster)
|
|
286
|
+
solana.reason = 'no_cluster';
|
|
287
|
+
else if (!expectedGenesis)
|
|
288
|
+
solana.reason = 'unknown_cluster';
|
|
289
|
+
else {
|
|
290
|
+
const genesis = await fetchGenesis(solanaRpc);
|
|
291
|
+
if (genesis !== expectedGenesis)
|
|
292
|
+
solana.reason = 'cluster_mismatch';
|
|
293
|
+
}
|
|
294
|
+
const headSlot = input.head.anchors?.solana?.slot;
|
|
295
|
+
if (!solana.reason && headSlot != null && tx.slot != null && Number(tx.slot) !== Number(headSlot)) {
|
|
296
|
+
solana.reason = 'slot_mismatch';
|
|
297
|
+
}
|
|
298
|
+
if (!solana.reason) {
|
|
299
|
+
solana.valid = true;
|
|
300
|
+
solana.memo = matched[0];
|
|
301
|
+
if (tx.slot != null)
|
|
302
|
+
solana.slot = tx.slot;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
solana.reason = err instanceof Error ? err.message : 'solana_rpc_error';
|
|
308
|
+
}
|
|
309
|
+
if (!solana.valid && solana.reason)
|
|
310
|
+
errors.push(`solana:${solana.reason}`);
|
|
311
|
+
}
|
|
312
|
+
if (!txHash) {
|
|
313
|
+
base.reason = 'pending';
|
|
314
|
+
errors.push('base_pending');
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
base.checked = true;
|
|
318
|
+
try {
|
|
319
|
+
const fetchBase = input.fetchBaseTx || fetchBaseAnchorTransaction;
|
|
320
|
+
const tx = await fetchBase(txHash, baseRpc);
|
|
321
|
+
const expectedChain = input.head.anchors?.base?.chain_id || input.head.anchor?.chain_id || exports.BASE_MAINNET_CHAIN_ID;
|
|
322
|
+
if (!tx) {
|
|
323
|
+
base.reason = 'tx_not_found';
|
|
324
|
+
}
|
|
325
|
+
else if (tx.chainId != null && Number(tx.chainId) !== Number(expectedChain)) {
|
|
326
|
+
base.reason = 'chain_mismatch';
|
|
327
|
+
base.chain_id = Number(tx.chainId);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
const inputData = String(tx.input || '').toLowerCase().replace(/^0x/, '');
|
|
331
|
+
if (inputData !== root)
|
|
332
|
+
base.reason = 'calldata_mismatch';
|
|
333
|
+
else {
|
|
334
|
+
base.valid = true;
|
|
335
|
+
base.tx = tx.hash || txHash;
|
|
336
|
+
base.chain_id = tx.chainId ?? expectedChain;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
base.reason = err instanceof Error ? err.message : 'base_rpc_error';
|
|
342
|
+
}
|
|
343
|
+
if (!base.valid && base.reason)
|
|
344
|
+
errors.push(`base:${base.reason}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
let overall;
|
|
348
|
+
if (!inclusionValid || (solana.checked && !solana.valid) || (base.checked && !base.valid))
|
|
349
|
+
overall = 'failed';
|
|
350
|
+
else if (solana.valid && base.valid)
|
|
351
|
+
overall = 'verified';
|
|
352
|
+
else
|
|
353
|
+
overall = 'partial';
|
|
354
|
+
return {
|
|
355
|
+
overall,
|
|
356
|
+
root,
|
|
357
|
+
inclusion: { valid: inclusionValid, leaf: leafHex, leaf_source: leafSource, reason: inclusionReason },
|
|
358
|
+
solana,
|
|
359
|
+
base,
|
|
360
|
+
proves: exports.ANCHOR_PROVES,
|
|
361
|
+
does_not_prove: doesNotProve,
|
|
362
|
+
errors,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
//# sourceMappingURL=anchor-witness.js.map
|