@xfuel/verify 0.2.1 → 0.3.1
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 +41 -3
- package/dist/anchor-witness.d.ts +155 -0
- package/dist/anchor-witness.d.ts.map +1 -0
- package/dist/anchor-witness.js +431 -0
- package/dist/anchor-witness.js.map +1 -0
- 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 +527 -15
- package/dist/cli.js.map +1 -1
- package/dist/epoch.d.ts +109 -0
- package/dist/epoch.d.ts.map +1 -0
- package/dist/epoch.js +209 -0
- package/dist/epoch.js.map +1 -0
- 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 +146 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +305 -24
- 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/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 +6 -4
package/README.md
CHANGED
|
@@ -25,14 +25,29 @@ 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
|
|
31
32
|
|
|
32
33
|
```bash
|
|
33
|
-
# Local binding verification
|
|
34
|
+
# Local binding verification.
|
|
35
|
+
# Issuer history is fetched unless you pass --issuer-history-file or --no-issuer-history.
|
|
36
|
+
# A payload v10 receipt fails offline (exit 1) when that fetch cannot run.
|
|
34
37
|
npx xfuel-verify receipt.json
|
|
35
38
|
|
|
39
|
+
# Explicit offline. --no-issuer-history skips the kid window.
|
|
40
|
+
npx xfuel-verify receipt.json --no-issuer-history
|
|
41
|
+
|
|
42
|
+
# Hash the canonical object (must match signed payload_hash) and read not_after
|
|
43
|
+
# from a pinned issuer-history snapshot. Both flags are offline.
|
|
44
|
+
npx xfuel-verify receipt.json --canonical-preimage preimage.json --issuer-history-file issuer-history.json
|
|
45
|
+
|
|
46
|
+
# A signed refusal (schema chit402.refusal.v1) is not a payment.
|
|
47
|
+
# The check proves the issuer refused, at the signed anchor, for that code.
|
|
48
|
+
# It does not prove a payment or that the block still stands.
|
|
49
|
+
npx xfuel-verify refusal.json
|
|
50
|
+
|
|
36
51
|
# With on-chain nullifier check (requires network)
|
|
37
52
|
npx xfuel-verify receipt.json --check-nullifier
|
|
38
53
|
|
|
@@ -41,6 +56,9 @@ npx xfuel-verify receipt.json --json
|
|
|
41
56
|
|
|
42
57
|
# From stdin
|
|
43
58
|
curl -s https://api.chit402.com/receipt/task-123?format=json | npx xfuel-verify -
|
|
59
|
+
|
|
60
|
+
# Dual anchor: inclusion, Solana memo, Base calldata
|
|
61
|
+
npx xfuel-verify receipt.json inclusion.json head.json --rpc
|
|
44
62
|
```
|
|
45
63
|
|
|
46
64
|
## What This Verifies
|
|
@@ -52,6 +70,9 @@ curl -s https://api.chit402.com/receipt/task-123?format=json | npx xfuel-verify
|
|
|
52
70
|
| Output hash | No | Hash is on the receipt |
|
|
53
71
|
| On-chain settlement | Yes | Query Base RPC for tx |
|
|
54
72
|
| Nullifier anchor | Yes | Query ZKVerifierSP1 contract |
|
|
73
|
+
| Anchored receipt root | Yes, with `--rpc` | Inclusion proof, then Solana memo and Base calldata for that root |
|
|
74
|
+
| Canonical preimage | No | SHA-256 of `--canonical-preimage`, or the stored canonical object, matches signed `payload_hash` |
|
|
75
|
+
| Issuer history | Yes, unless `--no-issuer-history` or `--issuer-history-file` | `iat` is inside the kid's `not_before` / `not_after`. A payload v10 pin must match the snapshot hash. Offline v10 fails if the fetch cannot run |
|
|
55
76
|
|
|
56
77
|
## Issuer Signature Verification (ES256)
|
|
57
78
|
|
|
@@ -65,13 +86,26 @@ counts only when the verifying key is trusted:
|
|
|
65
86
|
production kid `IvFpmC-vPhkY_v0vidsrWVT9uzlE5XWKZgAEOeJTq1Q`. Override with
|
|
66
87
|
`--trusted-kid`, or disable with `--no-trusted-kid`.
|
|
67
88
|
|
|
89
|
+
The epoch record checked by `xfuel-verify receipt.json inclusion.json head.json --rpc` uses those same sources. An embedded epoch key still has to match the trusted-kid pin.
|
|
90
|
+
|
|
68
91
|
`issuer_jwk` on the receipt is not a trust root. A copy re-signed with an
|
|
69
92
|
arbitrary P-256 key reports `key untrusted` (`issuer_signature.valid === false`),
|
|
70
93
|
including when `--jwks-file` points at the real JWKS.
|
|
71
94
|
|
|
72
95
|
```bash
|
|
73
|
-
#
|
|
74
|
-
npx xfuel-verify receipt.json
|
|
96
|
+
# Explicit offline. The default command fetches issuer history.
|
|
97
|
+
npx xfuel-verify receipt.json --no-issuer-history
|
|
98
|
+
|
|
99
|
+
# Recompute a published canonical preimage and check the kid window.
|
|
100
|
+
# curl -f stops on HTTP errors. Most receipts have no /preimage (a 404).
|
|
101
|
+
# A missing preimage is not a verification failure: omit --canonical-preimage.
|
|
102
|
+
curl -fsS "https://api.chit402.com/receipt/RECEIPT_ID?format=json" -o receipt.json
|
|
103
|
+
curl -fsS "https://api.chit402.com/.well-known/issuer-history.json?version=1" -o issuer-history.json
|
|
104
|
+
if curl -fsS "https://api.chit402.com/receipt/RECEIPT_ID/preimage" -o preimage.json; then
|
|
105
|
+
npx xfuel-verify receipt.json --canonical-preimage preimage.json --issuer-history-file issuer-history.json
|
|
106
|
+
else
|
|
107
|
+
npx xfuel-verify receipt.json --issuer-history-file issuer-history.json
|
|
108
|
+
fi
|
|
75
109
|
|
|
76
110
|
# Trust the published JWKS instead of (or in addition to) the pin
|
|
77
111
|
curl -o issuer-jwks.json https://api.chit402.com/.well-known/jwks.json
|
|
@@ -199,6 +233,10 @@ Full verification combining binding and optional nullifier check.
|
|
|
199
233
|
}
|
|
200
234
|
```
|
|
201
235
|
|
|
236
|
+
## Build digest
|
|
237
|
+
|
|
238
|
+
`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.
|
|
239
|
+
|
|
202
240
|
## License
|
|
203
241
|
|
|
204
242
|
Apache-2.0
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { type EpochRecord } from './epoch.js';
|
|
2
|
+
export declare const MEMO_PROGRAM_ID = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
3
|
+
export declare const MEMO_PROGRAM_ID_V1 = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo";
|
|
4
|
+
/**
|
|
5
|
+
* Full `getGenesisHash` values. CAIP-2 references keep only the first 32
|
|
6
|
+
* characters, and `getGenesisHash` returns the whole base58 hash. A prefix
|
|
7
|
+
* compare never matches mainnet-beta.
|
|
8
|
+
*/
|
|
9
|
+
export declare const SOLANA_GENESIS: Record<string, string>;
|
|
10
|
+
export declare const BASE_MAINNET_CHAIN_ID = 8453;
|
|
11
|
+
export declare const ANCHOR_PROVES: string[];
|
|
12
|
+
export declare const ANCHOR_DOES_NOT_PROVE: string[];
|
|
13
|
+
export interface InclusionStep {
|
|
14
|
+
hash: string;
|
|
15
|
+
position: string;
|
|
16
|
+
}
|
|
17
|
+
export interface AnchorReceipt {
|
|
18
|
+
task_id?: string;
|
|
19
|
+
row_hash?: string | null;
|
|
20
|
+
book_chain?: {
|
|
21
|
+
row_hash?: string | null;
|
|
22
|
+
} | null;
|
|
23
|
+
}
|
|
24
|
+
export interface AnchorInclusion {
|
|
25
|
+
task_id?: string;
|
|
26
|
+
leaf_index: number;
|
|
27
|
+
tree_size: number;
|
|
28
|
+
root: string;
|
|
29
|
+
leaf?: string;
|
|
30
|
+
row_hash?: string | null;
|
|
31
|
+
proof: InclusionStep[];
|
|
32
|
+
}
|
|
33
|
+
export interface AnchorHead {
|
|
34
|
+
root: string;
|
|
35
|
+
tree_size?: number;
|
|
36
|
+
epoch?: number | null;
|
|
37
|
+
prev_epoch_root?: string | null;
|
|
38
|
+
prev_epoch_size?: number | null;
|
|
39
|
+
anchor_tx?: string | null;
|
|
40
|
+
anchor?: {
|
|
41
|
+
tx?: string | null;
|
|
42
|
+
calldata?: string | null;
|
|
43
|
+
chain_id?: number | null;
|
|
44
|
+
} | null;
|
|
45
|
+
anchors?: {
|
|
46
|
+
base?: {
|
|
47
|
+
tx?: string | null;
|
|
48
|
+
calldata?: string | null;
|
|
49
|
+
chain_id?: number | null;
|
|
50
|
+
status?: string;
|
|
51
|
+
} | null;
|
|
52
|
+
solana?: {
|
|
53
|
+
signature?: string | null;
|
|
54
|
+
slot?: number | null;
|
|
55
|
+
cluster?: string | null;
|
|
56
|
+
memo?: string | null;
|
|
57
|
+
status?: string | null;
|
|
58
|
+
} | null;
|
|
59
|
+
} | null;
|
|
60
|
+
}
|
|
61
|
+
interface SolanaIx {
|
|
62
|
+
program?: string;
|
|
63
|
+
programId?: string;
|
|
64
|
+
parsed?: string | {
|
|
65
|
+
memo?: string;
|
|
66
|
+
info?: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export interface SolanaAnchorTx {
|
|
70
|
+
slot?: number;
|
|
71
|
+
meta?: {
|
|
72
|
+
err?: unknown;
|
|
73
|
+
logMessages?: string[];
|
|
74
|
+
innerInstructions?: Array<{
|
|
75
|
+
instructions?: SolanaIx[];
|
|
76
|
+
}>;
|
|
77
|
+
} | null;
|
|
78
|
+
transaction?: {
|
|
79
|
+
message?: {
|
|
80
|
+
instructions?: SolanaIx[];
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export interface BaseAnchorTx {
|
|
85
|
+
hash?: string;
|
|
86
|
+
input?: string;
|
|
87
|
+
chainId?: number;
|
|
88
|
+
}
|
|
89
|
+
export interface AnchorWitnessResult {
|
|
90
|
+
overall: 'verified' | 'partial' | 'failed';
|
|
91
|
+
root: string | null;
|
|
92
|
+
inclusion: {
|
|
93
|
+
valid: boolean;
|
|
94
|
+
leaf: string | null;
|
|
95
|
+
leaf_source: string;
|
|
96
|
+
reason?: string;
|
|
97
|
+
};
|
|
98
|
+
solana: {
|
|
99
|
+
checked: boolean;
|
|
100
|
+
valid: boolean;
|
|
101
|
+
signature: string | null;
|
|
102
|
+
slot: number | null;
|
|
103
|
+
cluster: string | null;
|
|
104
|
+
memo: string | null;
|
|
105
|
+
reason?: string;
|
|
106
|
+
};
|
|
107
|
+
base: {
|
|
108
|
+
checked: boolean;
|
|
109
|
+
valid: boolean;
|
|
110
|
+
tx: string | null;
|
|
111
|
+
chain_id: number | null;
|
|
112
|
+
reason?: string;
|
|
113
|
+
};
|
|
114
|
+
proves: string[];
|
|
115
|
+
does_not_prove: string[];
|
|
116
|
+
errors: string[];
|
|
117
|
+
}
|
|
118
|
+
export declare function leafHash(bytes: Uint8Array): Buffer;
|
|
119
|
+
export declare function normalizeRoot(root: string | null | undefined): string | null;
|
|
120
|
+
/** RFC 6962-style inclusion. Same rule as the gateway tree. */
|
|
121
|
+
export declare function verifyMerkleInclusion(leaf: Buffer, index: number, treeSize: number, rootHex: string, proof: InclusionStep[]): boolean;
|
|
122
|
+
export interface ParsedAnchorMemo {
|
|
123
|
+
version?: 1 | 2;
|
|
124
|
+
scope: string;
|
|
125
|
+
day: string;
|
|
126
|
+
root: string;
|
|
127
|
+
prev: string;
|
|
128
|
+
epoch?: number;
|
|
129
|
+
prev_epoch_root?: string;
|
|
130
|
+
prev_epoch_size?: number;
|
|
131
|
+
bundle_index_hash?: string;
|
|
132
|
+
}
|
|
133
|
+
/** v1 memos stay valid. v2 adds epoch, prev_epoch_root, prev_epoch_size, and the bundle index hash. */
|
|
134
|
+
export declare function parseAnchorMemo(memo: string): ParsedAnchorMemo | null;
|
|
135
|
+
export declare function extractMemos(tx: SolanaAnchorTx | null | undefined): string[];
|
|
136
|
+
export declare function fetchSolanaGenesisHash(rpcUrl: string): Promise<string>;
|
|
137
|
+
export declare function fetchBaseAnchorTransaction(txHash: string, rpcUrl: string): Promise<BaseAnchorTx | null>;
|
|
138
|
+
export interface VerifyAnchoredRootInput {
|
|
139
|
+
receipt: AnchorReceipt;
|
|
140
|
+
inclusion: AnchorInclusion;
|
|
141
|
+
head: AnchorHead;
|
|
142
|
+
baseRpcUrl?: string;
|
|
143
|
+
solanaRpcUrl?: string;
|
|
144
|
+
fetchSolanaTx?: (signature: string, rpcUrl: string) => Promise<SolanaAnchorTx | null>;
|
|
145
|
+
fetchGenesis?: (rpcUrl: string) => Promise<string>;
|
|
146
|
+
fetchBaseTx?: (txHash: string, rpcUrl: string) => Promise<BaseAnchorTx | null>;
|
|
147
|
+
epochRecord?: EpochRecord | null;
|
|
148
|
+
verifyEpochSignature?: (jws: string) => boolean;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Verify inclusion, then the Solana memo and the Base calldata for that root.
|
|
152
|
+
*/
|
|
153
|
+
export declare function verifyAnchoredRoot(input: VerifyAnchoredRootInput): Promise<AnchorWitnessResult>;
|
|
154
|
+
export {};
|
|
155
|
+
//# 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,431 @@
|
|
|
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 epoch_js_1 = require("./epoch.js");
|
|
23
|
+
const solana_payer_js_1 = require("./solana-payer.js");
|
|
24
|
+
exports.MEMO_PROGRAM_ID = 'MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr';
|
|
25
|
+
exports.MEMO_PROGRAM_ID_V1 = 'Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo';
|
|
26
|
+
/**
|
|
27
|
+
* Full `getGenesisHash` values. CAIP-2 references keep only the first 32
|
|
28
|
+
* characters, and `getGenesisHash` returns the whole base58 hash. A prefix
|
|
29
|
+
* compare never matches mainnet-beta.
|
|
30
|
+
*/
|
|
31
|
+
exports.SOLANA_GENESIS = {
|
|
32
|
+
'mainnet-beta': '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d',
|
|
33
|
+
devnet: 'EtWTRABZaYq6iMfeYKouRu166VU2xqa1wcaWoxPkrZBG',
|
|
34
|
+
testnet: '4uhcVJyU9pJkvQyS88uRDiswHXSCkY3zQawwpjk2NsNY',
|
|
35
|
+
};
|
|
36
|
+
exports.BASE_MAINNET_CHAIN_ID = 8453;
|
|
37
|
+
exports.ANCHOR_PROVES = [
|
|
38
|
+
'This receipt leaf is inside the issuer append-only tree of the stated size, under the stated root.',
|
|
39
|
+
'The Solana transaction memo contains that root, and the RPC genesis hash is the cluster named on the head.',
|
|
40
|
+
'The Base transaction calldata is that same root, on chain id 8453.',
|
|
41
|
+
];
|
|
42
|
+
exports.ANCHOR_DOES_NOT_PROVE = [
|
|
43
|
+
'It does not prove the payment, the payer, the payee, or the model output.',
|
|
44
|
+
'It does not prove a receipt appended after this tree size is inside the root.',
|
|
45
|
+
'It does not prove the issuer included only honest rows. It proves this leaf hashes into the published root.',
|
|
46
|
+
'It does not prove the RPC itself is honest. It checks the transaction and genesis hash that RPC returned.',
|
|
47
|
+
];
|
|
48
|
+
function sha256(buf) {
|
|
49
|
+
return (0, node_crypto_1.createHash)('sha256').update(buf).digest();
|
|
50
|
+
}
|
|
51
|
+
function leafHash(bytes) {
|
|
52
|
+
return sha256(Buffer.concat([Buffer.from([0x00]), Buffer.from(bytes)]));
|
|
53
|
+
}
|
|
54
|
+
function nodeHash(left, right) {
|
|
55
|
+
return sha256(Buffer.concat([Buffer.from([0x01]), left, right]));
|
|
56
|
+
}
|
|
57
|
+
function normalizeRoot(root) {
|
|
58
|
+
const hex = String(root || '').replace(/^0x/, '').toLowerCase();
|
|
59
|
+
return /^[0-9a-f]{64}$/.test(hex) ? hex : null;
|
|
60
|
+
}
|
|
61
|
+
/** RFC 6962-style inclusion. Same rule as the gateway tree. */
|
|
62
|
+
function verifyMerkleInclusion(leaf, index, treeSize, rootHex, proof) {
|
|
63
|
+
if (!Array.isArray(proof) || index < 0 || index >= treeSize)
|
|
64
|
+
return false;
|
|
65
|
+
let hash = new Uint8Array(leaf);
|
|
66
|
+
for (const step of proof) {
|
|
67
|
+
if (!step || !/^[0-9a-fA-F]{64}$/.test(step.hash))
|
|
68
|
+
return false;
|
|
69
|
+
const sib = Buffer.from(step.hash, 'hex');
|
|
70
|
+
const next = step.position === 'left' ? nodeHash(sib, hash) : nodeHash(hash, sib);
|
|
71
|
+
hash = new Uint8Array(next);
|
|
72
|
+
}
|
|
73
|
+
return Buffer.from(hash).toString('hex') === String(rootHex).replace(/^0x/, '').toLowerCase();
|
|
74
|
+
}
|
|
75
|
+
function memoIdentity(scope, day, root, prev) {
|
|
76
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(scope))
|
|
77
|
+
return null;
|
|
78
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(day))
|
|
79
|
+
return null;
|
|
80
|
+
if (!/^[0-9a-f]{64}$/.test(root) || !/^[0-9a-f]{64}$/.test(prev))
|
|
81
|
+
return null;
|
|
82
|
+
return { scope, day, root, prev };
|
|
83
|
+
}
|
|
84
|
+
/** v1 memos stay valid. v2 adds epoch, prev_epoch_root, prev_epoch_size, and the bundle index hash. */
|
|
85
|
+
function parseAnchorMemo(memo) {
|
|
86
|
+
const parts = String(memo || '').split(':');
|
|
87
|
+
if (parts[0] !== 'chit402' || parts[1] !== 'root')
|
|
88
|
+
return null;
|
|
89
|
+
if (parts.length === 7 && parts[2] === 'v1') {
|
|
90
|
+
const base = memoIdentity(parts[3], parts[4], parts[5], parts[6]);
|
|
91
|
+
return base ? { version: 1, ...base } : null;
|
|
92
|
+
}
|
|
93
|
+
if (parts.length === 11 && parts[2] === 'v2') {
|
|
94
|
+
const base = memoIdentity(parts[3], parts[4], parts[5], parts[6]);
|
|
95
|
+
if (!base)
|
|
96
|
+
return null;
|
|
97
|
+
const epoch = Number(parts[7]);
|
|
98
|
+
const prevEpoch = parts[8];
|
|
99
|
+
const size = Number(parts[9]);
|
|
100
|
+
const bundle = parts[10];
|
|
101
|
+
if (!Number.isInteger(epoch) || epoch < 1)
|
|
102
|
+
return null;
|
|
103
|
+
if (!/^[0-9a-f]{64}$/.test(prevEpoch) || !/^[0-9a-f]{64}$/.test(bundle))
|
|
104
|
+
return null;
|
|
105
|
+
if (!Number.isInteger(size) || size < 0)
|
|
106
|
+
return null;
|
|
107
|
+
return {
|
|
108
|
+
version: 2,
|
|
109
|
+
...base,
|
|
110
|
+
epoch,
|
|
111
|
+
prev_epoch_root: prevEpoch,
|
|
112
|
+
prev_epoch_size: size,
|
|
113
|
+
bundle_index_hash: bundle,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
function isMemoIx(ix) {
|
|
119
|
+
const program = ix.program || '';
|
|
120
|
+
const programId = ix.programId || '';
|
|
121
|
+
return program === 'spl-memo'
|
|
122
|
+
|| programId === exports.MEMO_PROGRAM_ID
|
|
123
|
+
|| programId === exports.MEMO_PROGRAM_ID_V1;
|
|
124
|
+
}
|
|
125
|
+
function extractMemos(tx) {
|
|
126
|
+
if (!tx)
|
|
127
|
+
return [];
|
|
128
|
+
const found = [];
|
|
129
|
+
const push = (value) => {
|
|
130
|
+
if (value && !found.includes(value))
|
|
131
|
+
found.push(value);
|
|
132
|
+
};
|
|
133
|
+
const visit = (ix) => {
|
|
134
|
+
if (!ix || !isMemoIx(ix))
|
|
135
|
+
return;
|
|
136
|
+
if (typeof ix.parsed === 'string')
|
|
137
|
+
push(ix.parsed);
|
|
138
|
+
else if (ix.parsed && typeof ix.parsed.memo === 'string')
|
|
139
|
+
push(ix.parsed.memo);
|
|
140
|
+
else if (ix.parsed && typeof ix.parsed.info === 'string')
|
|
141
|
+
push(ix.parsed.info);
|
|
142
|
+
};
|
|
143
|
+
for (const ix of tx.transaction?.message?.instructions || [])
|
|
144
|
+
visit(ix);
|
|
145
|
+
for (const inner of tx.meta?.innerInstructions || []) {
|
|
146
|
+
for (const ix of inner.instructions || [])
|
|
147
|
+
visit(ix);
|
|
148
|
+
}
|
|
149
|
+
for (const line of tx.meta?.logMessages || []) {
|
|
150
|
+
const match = /Memo \(len \d+\): "(.*)"/.exec(line);
|
|
151
|
+
if (match)
|
|
152
|
+
push(match[1]);
|
|
153
|
+
}
|
|
154
|
+
return found;
|
|
155
|
+
}
|
|
156
|
+
async function defaultRpc(url, method, params) {
|
|
157
|
+
const res = await fetch(url, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: { 'content-type': 'application/json' },
|
|
160
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
|
|
161
|
+
});
|
|
162
|
+
if (!res.ok)
|
|
163
|
+
throw new Error(`HTTP ${res.status}`);
|
|
164
|
+
const json = await res.json();
|
|
165
|
+
if (json.error)
|
|
166
|
+
throw new Error(json.error.message || 'rpc_error');
|
|
167
|
+
return json.result ?? null;
|
|
168
|
+
}
|
|
169
|
+
async function fetchSolanaGenesisHash(rpcUrl) {
|
|
170
|
+
const result = await defaultRpc(rpcUrl, 'getGenesisHash', []);
|
|
171
|
+
if (typeof result !== 'string' || result.length === 0)
|
|
172
|
+
throw new Error('no_genesis');
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
async function fetchBaseAnchorTransaction(txHash, rpcUrl) {
|
|
176
|
+
const tx = await defaultRpc(rpcUrl, 'eth_getTransactionByHash', [txHash]);
|
|
177
|
+
const chainHex = await defaultRpc(rpcUrl, 'eth_chainId', []);
|
|
178
|
+
if (!tx)
|
|
179
|
+
return null;
|
|
180
|
+
return {
|
|
181
|
+
hash: tx.hash,
|
|
182
|
+
input: tx.input,
|
|
183
|
+
chainId: chainHex ? Number(chainHex) : undefined,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function rowHashFrom(receipt, inclusion) {
|
|
187
|
+
if (typeof receipt.row_hash === 'string')
|
|
188
|
+
return { rowHash: receipt.row_hash, source: 'receipt' };
|
|
189
|
+
if (typeof receipt.book_chain?.row_hash === 'string')
|
|
190
|
+
return { rowHash: receipt.book_chain.row_hash, source: 'receipt' };
|
|
191
|
+
if (typeof inclusion.row_hash === 'string')
|
|
192
|
+
return { rowHash: inclusion.row_hash, source: 'inclusion' };
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
function solanaSignature(head) {
|
|
196
|
+
const sol = head.anchors?.solana;
|
|
197
|
+
if (!sol || sol.status === 'pending')
|
|
198
|
+
return null;
|
|
199
|
+
return sol.signature || null;
|
|
200
|
+
}
|
|
201
|
+
function baseTxHash(head) {
|
|
202
|
+
const base = head.anchors?.base;
|
|
203
|
+
if (base?.status === 'pending')
|
|
204
|
+
return null;
|
|
205
|
+
return base?.tx || head.anchor?.tx || head.anchor_tx || null;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Verify inclusion, then the Solana memo and the Base calldata for that root.
|
|
209
|
+
*/
|
|
210
|
+
async function verifyAnchoredRoot(input) {
|
|
211
|
+
const errors = [];
|
|
212
|
+
const doesNotProve = [...exports.ANCHOR_DOES_NOT_PROVE];
|
|
213
|
+
const root = normalizeRoot(input.head?.root);
|
|
214
|
+
const inclusionRoot = normalizeRoot(input.inclusion?.root);
|
|
215
|
+
let leafHex = null;
|
|
216
|
+
let leafSource = 'none';
|
|
217
|
+
let inclusionValid = false;
|
|
218
|
+
let inclusionReason;
|
|
219
|
+
const taskId = input.receipt?.task_id ? String(input.receipt.task_id) : '';
|
|
220
|
+
const inclusionError = input.inclusion?.error;
|
|
221
|
+
if (inclusionError === 'not_in_tree') {
|
|
222
|
+
inclusionReason = 'not_in_tree';
|
|
223
|
+
}
|
|
224
|
+
else if (!taskId) {
|
|
225
|
+
inclusionReason = 'missing_task_id';
|
|
226
|
+
}
|
|
227
|
+
else if (input.inclusion?.task_id && String(input.inclusion.task_id) !== taskId) {
|
|
228
|
+
inclusionReason = 'task_mismatch';
|
|
229
|
+
}
|
|
230
|
+
else if (!root || !inclusionRoot) {
|
|
231
|
+
inclusionReason = 'bad_root';
|
|
232
|
+
}
|
|
233
|
+
else if (root !== inclusionRoot) {
|
|
234
|
+
inclusionReason = 'root_mismatch';
|
|
235
|
+
}
|
|
236
|
+
else if (input.head.tree_size != null && Number(input.head.tree_size) !== Number(input.inclusion.tree_size)) {
|
|
237
|
+
inclusionReason = 'tree_size_mismatch';
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
const row = rowHashFrom(input.receipt, input.inclusion);
|
|
241
|
+
let leaf = null;
|
|
242
|
+
if (row) {
|
|
243
|
+
leaf = leafHash(Buffer.from(`${taskId}|${row.rowHash}`));
|
|
244
|
+
leafSource = row.source;
|
|
245
|
+
if (input.inclusion.leaf && input.inclusion.leaf.toLowerCase() !== leaf.toString('hex')) {
|
|
246
|
+
inclusionReason = 'leaf_mismatch';
|
|
247
|
+
leaf = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else if (input.inclusion.leaf && /^[0-9a-fA-F]{64}$/.test(input.inclusion.leaf)) {
|
|
251
|
+
leaf = Buffer.from(input.inclusion.leaf, 'hex');
|
|
252
|
+
leafSource = 'inclusion';
|
|
253
|
+
doesNotProve.push('The leaf was taken from the inclusion object. The receipt had no row_hash, so this check did not recompute the leaf from the receipt bytes.');
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
inclusionReason = 'no_leaf';
|
|
257
|
+
}
|
|
258
|
+
if (leaf) {
|
|
259
|
+
leafHex = leaf.toString('hex');
|
|
260
|
+
inclusionValid = verifyMerkleInclusion(leaf, Number(input.inclusion.leaf_index), Number(input.inclusion.tree_size), root, input.inclusion.proof || []);
|
|
261
|
+
if (!inclusionValid)
|
|
262
|
+
inclusionReason = 'inclusion_failed';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!inclusionValid && inclusionReason)
|
|
266
|
+
errors.push(inclusionReason);
|
|
267
|
+
let epochReason;
|
|
268
|
+
if (input.head?.epoch == null) {
|
|
269
|
+
const link = (0, epoch_js_1.verifyEpochLink)(input.head, null);
|
|
270
|
+
if (!link.ok)
|
|
271
|
+
epochReason = link.reason || 'epoch_missing';
|
|
272
|
+
}
|
|
273
|
+
const needsEpoch = input.head?.epoch != null || input.epochRecord != null;
|
|
274
|
+
if (!epochReason && needsEpoch) {
|
|
275
|
+
if (!input.epochRecord)
|
|
276
|
+
epochReason = 'epoch_record_missing';
|
|
277
|
+
else {
|
|
278
|
+
const checked = (0, epoch_js_1.verifyEpochRecord)(input.epochRecord, { verifySignature: input.verifyEpochSignature });
|
|
279
|
+
if (!checked.ok)
|
|
280
|
+
epochReason = checked.reason || 'epoch_record';
|
|
281
|
+
else if (input.head?.epoch != null && Number(input.head.epoch) > 1) {
|
|
282
|
+
const prev = (input.epochRecord.epochs || []).find((row) => Number(row.epoch) === Number(input.head.epoch) - 1);
|
|
283
|
+
const link = (0, epoch_js_1.verifyEpochLink)(input.head, prev ? {
|
|
284
|
+
root: prev.final_root || prev.opening_root || null,
|
|
285
|
+
tree_size: prev.final_size ?? prev.opening_size ?? null,
|
|
286
|
+
} : null);
|
|
287
|
+
if (!link.ok)
|
|
288
|
+
epochReason = link.reason || 'epoch_link';
|
|
289
|
+
}
|
|
290
|
+
else if (input.head) {
|
|
291
|
+
const link = (0, epoch_js_1.verifyEpochLink)(input.head, null);
|
|
292
|
+
if (!link.ok)
|
|
293
|
+
epochReason = link.reason || 'epoch_link';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (epochReason)
|
|
298
|
+
errors.push(epochReason);
|
|
299
|
+
const solanaRpc = input.solanaRpcUrl || process.env.SOLANA_RPC_URL || solana_payer_js_1.SOLANA_RPC_URL;
|
|
300
|
+
const baseRpc = input.baseRpcUrl || base_payer_js_1.BASE_RPC_URL;
|
|
301
|
+
const signature = root && inclusionValid ? solanaSignature(input.head) : null;
|
|
302
|
+
const txHash = root && inclusionValid ? baseTxHash(input.head) : null;
|
|
303
|
+
const solana = {
|
|
304
|
+
checked: false,
|
|
305
|
+
valid: false,
|
|
306
|
+
signature: input.head?.anchors?.solana?.signature ?? null,
|
|
307
|
+
slot: input.head?.anchors?.solana?.slot ?? null,
|
|
308
|
+
cluster: input.head?.anchors?.solana?.cluster ?? null,
|
|
309
|
+
memo: input.head?.anchors?.solana?.memo ?? null,
|
|
310
|
+
reason: undefined,
|
|
311
|
+
};
|
|
312
|
+
const base = {
|
|
313
|
+
checked: false,
|
|
314
|
+
valid: false,
|
|
315
|
+
tx: input.head?.anchors?.base?.tx || input.head?.anchor?.tx || input.head?.anchor_tx || null,
|
|
316
|
+
chain_id: input.head?.anchors?.base?.chain_id ?? input.head?.anchor?.chain_id ?? null,
|
|
317
|
+
reason: undefined,
|
|
318
|
+
};
|
|
319
|
+
if (!inclusionValid) {
|
|
320
|
+
solana.reason = 'inclusion_failed';
|
|
321
|
+
base.reason = 'inclusion_failed';
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
if (!signature) {
|
|
325
|
+
solana.reason = 'pending';
|
|
326
|
+
errors.push('solana_pending');
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
solana.checked = true;
|
|
330
|
+
try {
|
|
331
|
+
const fetchTx = input.fetchSolanaTx || ((sig, url) => (0, solana_payer_js_1.fetchSolanaTransaction)(sig, url));
|
|
332
|
+
const fetchGenesis = input.fetchGenesis || fetchSolanaGenesisHash;
|
|
333
|
+
const tx = await fetchTx(signature, solanaRpc);
|
|
334
|
+
if (!tx) {
|
|
335
|
+
solana.reason = 'tx_not_found';
|
|
336
|
+
}
|
|
337
|
+
else if (tx.meta?.err) {
|
|
338
|
+
solana.reason = 'tx_failed';
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const memos = extractMemos(tx);
|
|
342
|
+
const recorded = input.head.anchors?.solana?.memo || null;
|
|
343
|
+
const matched = memos.filter((memo) => {
|
|
344
|
+
const parsed = parseAnchorMemo(memo);
|
|
345
|
+
return Boolean(parsed && parsed.root === root && memo.includes(root) && (!recorded || memo === recorded));
|
|
346
|
+
});
|
|
347
|
+
const cluster = input.head.anchors?.solana?.cluster || null;
|
|
348
|
+
const expectedGenesis = cluster ? exports.SOLANA_GENESIS[cluster] : null;
|
|
349
|
+
if (matched.length === 0)
|
|
350
|
+
solana.reason = 'memo_mismatch';
|
|
351
|
+
else if (!cluster)
|
|
352
|
+
solana.reason = 'no_cluster';
|
|
353
|
+
else if (!expectedGenesis)
|
|
354
|
+
solana.reason = 'unknown_cluster';
|
|
355
|
+
else {
|
|
356
|
+
const genesis = await fetchGenesis(solanaRpc);
|
|
357
|
+
if (genesis !== expectedGenesis)
|
|
358
|
+
solana.reason = 'cluster_mismatch';
|
|
359
|
+
}
|
|
360
|
+
const headSlot = input.head.anchors?.solana?.slot;
|
|
361
|
+
if (!solana.reason && headSlot != null && tx.slot != null && Number(tx.slot) !== Number(headSlot)) {
|
|
362
|
+
solana.reason = 'slot_mismatch';
|
|
363
|
+
}
|
|
364
|
+
if (!solana.reason) {
|
|
365
|
+
solana.valid = true;
|
|
366
|
+
solana.memo = matched[0];
|
|
367
|
+
if (tx.slot != null)
|
|
368
|
+
solana.slot = tx.slot;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
solana.reason = err instanceof Error ? err.message : 'solana_rpc_error';
|
|
374
|
+
}
|
|
375
|
+
if (!solana.valid && solana.reason)
|
|
376
|
+
errors.push(`solana:${solana.reason}`);
|
|
377
|
+
}
|
|
378
|
+
if (!txHash) {
|
|
379
|
+
base.reason = 'pending';
|
|
380
|
+
errors.push('base_pending');
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
base.checked = true;
|
|
384
|
+
try {
|
|
385
|
+
const fetchBase = input.fetchBaseTx || fetchBaseAnchorTransaction;
|
|
386
|
+
const tx = await fetchBase(txHash, baseRpc);
|
|
387
|
+
const expectedChain = input.head.anchors?.base?.chain_id || input.head.anchor?.chain_id || exports.BASE_MAINNET_CHAIN_ID;
|
|
388
|
+
if (!tx) {
|
|
389
|
+
base.reason = 'tx_not_found';
|
|
390
|
+
}
|
|
391
|
+
else if (tx.chainId != null && Number(tx.chainId) !== Number(expectedChain)) {
|
|
392
|
+
base.reason = 'chain_mismatch';
|
|
393
|
+
base.chain_id = Number(tx.chainId);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
const inputData = String(tx.input || '').toLowerCase().replace(/^0x/, '');
|
|
397
|
+
if (inputData !== root)
|
|
398
|
+
base.reason = 'calldata_mismatch';
|
|
399
|
+
else {
|
|
400
|
+
base.valid = true;
|
|
401
|
+
base.tx = tx.hash || txHash;
|
|
402
|
+
base.chain_id = tx.chainId ?? expectedChain;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
base.reason = err instanceof Error ? err.message : 'base_rpc_error';
|
|
408
|
+
}
|
|
409
|
+
if (!base.valid && base.reason)
|
|
410
|
+
errors.push(`base:${base.reason}`);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
let overall;
|
|
414
|
+
if (!inclusionValid || epochReason || (solana.checked && !solana.valid) || (base.checked && !base.valid))
|
|
415
|
+
overall = 'failed';
|
|
416
|
+
else if (solana.valid && base.valid)
|
|
417
|
+
overall = 'verified';
|
|
418
|
+
else
|
|
419
|
+
overall = 'partial';
|
|
420
|
+
return {
|
|
421
|
+
overall,
|
|
422
|
+
root,
|
|
423
|
+
inclusion: { valid: inclusionValid, leaf: leafHex, leaf_source: leafSource, reason: inclusionReason },
|
|
424
|
+
solana,
|
|
425
|
+
base,
|
|
426
|
+
proves: exports.ANCHOR_PROVES,
|
|
427
|
+
does_not_prove: doesNotProve,
|
|
428
|
+
errors,
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
//# sourceMappingURL=anchor-witness.js.map
|