ocp-verify 1.0.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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Damon Zwicker
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,332 @@
1
+ # Observation Commitment Protocol (OCP)
2
+
3
+ If one byte changes, verification fails — across any chain, any system.
4
+
5
+ A chain-agnostic primitive for independently verifying that a specific byte sequence was committed to a public ledger.
6
+
7
+ Minimal. Verifiable. System-independent.
8
+
9
+ ---
10
+
11
+ ## ⚡ 30-Second Demo (start here)
12
+
13
+ ```bash
14
+ cd examples/oh-shit-demo
15
+ ./run-demo.sh
16
+ ```
17
+
18
+ Expected:
19
+
20
+ ```
21
+ VALID
22
+ INVALID
23
+ ```
24
+
25
+ If one byte changes, verification fails — across any system.
26
+
27
+ ---
28
+
29
+ ## 🔌 Integrate in 2 Minutes
30
+
31
+ ### 1) Commit (produce a proof)
32
+
33
+ ```bash
34
+ npx ocp-commit report.txt
35
+ ```
36
+
37
+ This automatically creates:
38
+
39
+ ```bash
40
+ report.proof.json
41
+ ```
42
+
43
+ ### 2) Verify (anywhere, later)
44
+
45
+ ```bash
46
+ npx ocp-verify report.txt
47
+ ```
48
+
49
+ Expected:
50
+
51
+ ```
52
+ VALID
53
+ ```
54
+
55
+ If any byte changes:
56
+
57
+ ```
58
+ INVALID: hash mismatch
59
+ ```
60
+
61
+ ### 3) Test tampering (optional)
62
+
63
+ ```bash
64
+ npx ocp-verify tampered.txt report.proof.json
65
+ ```
66
+
67
+ ### 4) Use in your system
68
+
69
+ - Save the file + proof together
70
+ - Or store the proof alongside records/logs
71
+ - Verification requires only the file and the proof
72
+
73
+ No API. No platform dependency.
74
+
75
+ ---
76
+
77
+ ## The Problem
78
+
79
+ Every AI system running today has the same problem.
80
+
81
+ You can't verify what it did.
82
+
83
+ Not really. You can ask the platform. You can trust the logs. You can hope the provider is telling the truth. But there is no independent, tamper-proof record of what an AI received, what it produced, and whether anything was changed in between.
84
+
85
+ Most digital systems can prove things — but only **inside themselves**.
86
+
87
+ Step outside the system, and verification depends on:
88
+ - APIs
89
+ - platforms
90
+ - intermediaries
91
+
92
+ OCP eliminates that dependency entirely.
93
+
94
+ ---
95
+
96
+ ## Where This Breaks Without OCP
97
+
98
+ - AI outputs cannot be independently verified
99
+ - Legal evidence depends on originating systems
100
+ - APIs and platforms are non-permanent
101
+ - Digital artifacts become disputable over time
102
+
103
+ Without a system-independent verification boundary,
104
+ "what actually happened" becomes ambiguous.
105
+
106
+ ---
107
+
108
+ ## Use Cases
109
+
110
+ OCP can be used anywhere a digital artifact may need to be independently verified later:
111
+
112
+ - AI outputs and execution traces
113
+ - Legal evidence and filings
114
+ - Audit logs and compliance records
115
+ - Media provenance
116
+ - File integrity
117
+ - Institutional records
118
+
119
+ ### Example: Verifying an AI Output
120
+
121
+ An AI system generates a report:
122
+
123
+ ```text
124
+ AI Risk Assessment: MEDIUM_RISK
125
+ Approved for internal review.
126
+ ```
127
+
128
+ That output is committed using OCP.
129
+
130
+ Later, the report is modified:
131
+
132
+ ```text
133
+ AI Risk Assessment: LOW_RISK
134
+ Approved for internal review.
135
+ ```
136
+
137
+ Using the original proof:
138
+
139
+ - the original output verifies as VALID
140
+ - the modified output returns INVALID
141
+
142
+ The difference is one word.
143
+
144
+ Verification does not depend on the AI system, API, or platform.
145
+
146
+ It depends only on the bytes.
147
+
148
+ ---
149
+
150
+ ## The Protocol
151
+
152
+ An observation is any byte sequence.
153
+
154
+ ```
155
+ data → digest → public commitment
156
+ ```
157
+
158
+ Verification reduces to:
159
+
160
+ ```
161
+ recompute → compare → confirm inclusion
162
+ ```
163
+
164
+ A verifier:
165
+ - recomputes the digest
166
+ - compares it to the committed value
167
+ - confirms that the digest exists in a referenced transaction
168
+
169
+ No API. No platform dependency. No trust in the originating system.
170
+
171
+ ---
172
+
173
+ ## Proof Envelope
174
+
175
+ OCP proofs are self-describing, chain-agnostic JSON artifacts. A valid proof envelope is verifiable against raw ledger data — no SDK, no RPC provider, no indexer required.
176
+
177
+ ```json
178
+ {
179
+ "ocp": "1.0",
180
+ "chain": {
181
+ "id": "eip155:84532",
182
+ "namespace": "evm"
183
+ },
184
+ "commitment": {
185
+ "digest": "14cca453684a18c1ef3e1c0b9a7744cfa06942660719bba373ef5fc36208bf73",
186
+ "hash_function": "sha2-256",
187
+ "serialization": "raw-bytes"
188
+ },
189
+ "ledger_ref": {
190
+ "transaction_id": "0xf2e1f6c085768b4e3d60463717d52bb2a338803a74a4cfd48aea5738d2595ddd",
191
+ "block_height": 41658348,
192
+ "block_hash": "0x...",
193
+ "finality": {
194
+ "depth": 3,
195
+ "assertion_time_utc": "2026-05-17T12:00:00Z"
196
+ }
197
+ },
198
+ "extraction": {
199
+ "rule_id": "evm/event-log",
200
+ "rule_version": "1.0.0"
201
+ },
202
+ "meta": {
203
+ "created_utc": "2026-05-17T12:00:05Z",
204
+ "envelope_version": "1.0"
205
+ }
206
+ }
207
+ ```
208
+
209
+ ---
210
+
211
+ ## Reference Implementation — Live on Base Sepolia
212
+
213
+ Contract: `0x0963Fd33DF80c94360F2DC22e5c09517AeE7ED5c`
214
+
215
+ ```solidity
216
+ contract ObservationCommitment {
217
+ event Recorded(bytes32 indexed digest, address indexed recorder);
218
+
219
+ function record(bytes32 digest) external {
220
+ emit Recorded(digest, msg.sender);
221
+ }
222
+ }
223
+ ```
224
+
225
+ Live verification — zero dependencies, Node.js stdlib only:
226
+
227
+ ```
228
+ hash MATCH 14cca453684a18c1ef3e1c0b9a7744cfa06942660719bba373ef5fc36208bf73
229
+ chain eip155:84532
230
+ rpc https://sepolia.base.org
231
+ tx 0xf2e1f6c085768b4e3d60463717d52bb2a338803a74a4cfd48aea5738d2595ddd
232
+ logs found 1 Recorded event(s)
233
+ digest MATCH 14cca453684a18c1ef3e1c0b9a7744cfa06942660719bba373ef5fc36208bf73
234
+
235
+ VALID
236
+ ```
237
+
238
+ ---
239
+
240
+ ## What OCP Defines
241
+
242
+ - A minimal verification model
243
+ - A system-independent verification boundary
244
+ - A portable, self-describing proof envelope (chain-agnostic)
245
+ - A formal extraction rule registry (`evm/event-log`, `solana/instruction-data`)
246
+
247
+ ---
248
+
249
+ ## What OCP Does Not Define
250
+
251
+ - Storage
252
+ - Identity
253
+ - Authorship
254
+ - Canonical encoding
255
+ - Application-layer semantics
256
+ - Sanitization or preprocessing pipelines
257
+
258
+ ---
259
+
260
+ ## In the Wild
261
+
262
+ OCP is being adopted as the Layer 3 commitment primitive in the ERC-8004 Universal AI Inference Verification Registry stack:
263
+
264
+ ```
265
+ L2 — input provenance: raw → sanitize → commit
266
+ L3 — OCP: digest → on-chain → verify from raw block
267
+ L4 — EIP-712 signed inference attestation
268
+ L5 — registry routes to zkML / opML / TEE
269
+ ```
270
+
271
+ The identity pipeline sentinel hash has been confirmed between two independent implementations:
272
+
273
+ ```
274
+ 8116eec29078e8f57c07077d5e8080a35bde73036581df3abb93755d1b1a16ea
275
+ ```
276
+
277
+ The full stack is live in production. L3 tx on Base Sepolia, block 41731493:
278
+ https://sepolia.basescan.org/tx/0xc3aeb16d0aef167e2ebc6d4afc9333fcd13a71b8c02e5485bc6be7491e393319
279
+
280
+ Thread: https://ethereum-magicians.org/t/draft-erc-universal-ai-inference-verification-registry/28083/20
281
+
282
+ ---
283
+
284
+ ## Why It Matters
285
+
286
+ OCP separates **verification from systems**.
287
+
288
+ A verifier does not ask what's true —
289
+ they compute it.
290
+
291
+ The network only confirms that a commitment exists.
292
+
293
+ ---
294
+
295
+ ## Start Here
296
+
297
+ - 📄 Core Specification → `/docs/spec/ocp-v1.0.0.md`
298
+ - 🗂️ Proof Envelope → `/docs/spec/ocp-proof-envelope-v1.0.0.md`
299
+ - ⛓️ EVM Extraction Rule → `/docs/spec/appendix-evm-r.md`
300
+ - 🔗 Solana Extraction Rule → `/docs/spec/appendix-solana-r.md`
301
+ - 🤖 AI Inference Attestation → `/docs/spec/appendix-ai-inference-attestation.md`
302
+ - 🧾 Proof Format → `/docs/spec/proof-format-v1.md`
303
+ - 🔍 Examples → `/examples`
304
+ - ⚙️ Contracts → `/contracts`
305
+ - 🌐 Live Demo → https://observation-commitment-protocol.vercel.app/
306
+
307
+ Reference implementation (VeraFile):
308
+ https://github.com/damonzwicker/verafile
309
+
310
+ ---
311
+
312
+ ## Quick Verify
313
+
314
+ ```bash
315
+ npx ocp-verify examples/example-observation.txt
316
+ ```
317
+
318
+ Expected output:
319
+
320
+ ```
321
+ VALID
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Status
327
+
328
+ v1.0.0 — Cross-Chain Primitive
329
+ Phase 1 complete — proof envelope schema
330
+ Phase 2 complete — EVM reference implementation live
331
+ Phase 3 complete — Solana appendix, chain-agnostic claim validated
332
+ First external contribution merged — dinamic.eth / ERC-8004 (PR #1)
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "ocp-verify",
3
+ "version": "1.0.0",
4
+ "description": "Zero-dependency verifier for the Observation Commitment Protocol — independently verify that a file was committed to a public blockchain",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "engines": {
8
+ "node": ">=18.0.0"
9
+ },
10
+ "bin": {
11
+ "ocp-commit": "reference-cli/commit.js",
12
+ "ocp-verify": "reference-cli/verify.js"
13
+ },
14
+ "files": [
15
+ "reference-cli",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "keywords": [
20
+ "ocp",
21
+ "observation-commitment-protocol",
22
+ "blockchain",
23
+ "verification",
24
+ "proof",
25
+ "ethereum",
26
+ "base",
27
+ "solana",
28
+ "ai-verification",
29
+ "zero-dependency",
30
+ "cryptography"
31
+ ],
32
+ "author": "Damon Zwicker",
33
+ "homepage": "https://github.com/damonzwicker/observation-commitment-protocol",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/damonzwicker/observation-commitment-protocol.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/damonzwicker/observation-commitment-protocol/issues"
40
+ }
41
+ }
@@ -0,0 +1,82 @@
1
+ # OCP Reference Verifier
2
+
3
+ A minimal, zero-dependency verifier for OCP proofs.
4
+
5
+ This script demonstrates the core verification invariant:
6
+
7
+ recompute → compare → confirm inclusion
8
+
9
+ ---
10
+
11
+ ## Usage
12
+
13
+ Run:
14
+
15
+ node verify.js <file> <proof.json>
16
+
17
+ Example:
18
+
19
+ node verify.js ../examples/example-observation.txt ../examples/example-proof.ocp.json
20
+
21
+ ---
22
+
23
+ ## What It Does
24
+
25
+ The verifier performs:
26
+
27
+ 1. Computes SHA-256 hash of the file
28
+ 2. Compares it to the `hash` field in the proof
29
+ 3. Outputs VALID or INVALID
30
+
31
+ ---
32
+
33
+ ## Output
34
+
35
+ If the file matches the proof:
36
+
37
+ VALID: file hash matches proof hash
38
+
39
+ If the file has been modified:
40
+
41
+ INVALID: hash mismatch
42
+
43
+ ---
44
+
45
+ ## Important
46
+
47
+ This verifier checks only:
48
+
49
+ - recompute
50
+ - compare
51
+
52
+ It does not perform:
53
+
54
+ - transaction lookup
55
+ - extraction rule execution
56
+ - on-chain verification
57
+
58
+ Those steps are defined in the protocol and must be performed independently.
59
+
60
+ ---
61
+
62
+ ## Purpose
63
+
64
+ This script exists to demonstrate:
65
+
66
+ - minimal verification logic
67
+ - independence from any platform or API
68
+ - reproducibility of OCP proofs
69
+
70
+ It is not a production tool.
71
+
72
+ ---
73
+
74
+ ## Next Steps
75
+
76
+ To fully verify a proof:
77
+
78
+ 1. Resolve `txHash` on the specified network
79
+ 2. Apply `extractionRule`
80
+ 3. Confirm the digest exists in the transaction
81
+
82
+ This completes the OCP verification process.
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const crypto = require("crypto");
5
+
6
+ const [, , filePath, proofArg] = process.argv;
7
+
8
+ if (!filePath) {
9
+ console.error("Usage: ocp-commit <file> [proof.json]");
10
+ process.exit(1);
11
+ }
12
+
13
+ if (!fs.existsSync(filePath)) {
14
+ console.error(`ERROR: file not found: ${filePath}`);
15
+ process.exit(1);
16
+ }
17
+
18
+ const proofPath =
19
+ proofArg ||
20
+ filePath.replace(/(\.[^/.]+)?$/, ".proof.json");
21
+
22
+ const fileBytes = fs.readFileSync(filePath);
23
+ const hash = "0x" + crypto.createHash("sha256").update(fileBytes).digest("hex");
24
+
25
+ const proof = {
26
+ version: "ocp-1",
27
+ hash,
28
+ txHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
29
+ network: "demo-local",
30
+ contract: "0x0000000000000000000000000000000000000000",
31
+ extractionRule: "demo:proof.hash"
32
+ };
33
+
34
+ fs.writeFileSync(proofPath, JSON.stringify(proof, null, 2) + "\n");
35
+
36
+ // Clean output
37
+ console.log(`COMMITTED: ${filePath} → ${proofPath}`);
@@ -0,0 +1,67 @@
1
+ // OCP Reference: Browser-native digest computation
2
+ // Uses Web Crypto API — no dependencies, no libraries
3
+ // Companion to reference-cli/verify.js (Node.js implementation)
4
+ //
5
+ // Spec: docs/spec/appendix-evm-r.md
6
+ // This is the write-side primitive: observation → digest
7
+ // The digest produced here is what gets passed to record(bytes32) on-chain
8
+
9
+ "use strict";
10
+
11
+ /**
12
+ * Compute the OCP digest of a File object.
13
+ *
14
+ * Implements the commitment procedure from appendix-evm-r.md:
15
+ * - Serialization: raw-bytes (no encoding applied)
16
+ * - Hash function: sha2-256
17
+ * - Output: lowercase hex string, no 0x prefix
18
+ *
19
+ * @param {File} file - Any File object (browser File API)
20
+ * @returns {Promise<string>} SHA-256 digest as lowercase hex, no 0x prefix
21
+ */
22
+ async function hashFile(file) {
23
+ const buffer = await file.arrayBuffer();
24
+ const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
25
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
26
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
27
+ }
28
+
29
+ /**
30
+ * Compute the OCP digest of an arbitrary byte array.
31
+ *
32
+ * @param {ArrayBuffer|Uint8Array} bytes - Raw bytes
33
+ * @returns {Promise<string>} SHA-256 digest as lowercase hex, no 0x prefix
34
+ */
35
+ async function hashBytes(bytes) {
36
+ const buffer = bytes instanceof ArrayBuffer ? bytes : bytes.buffer;
37
+ const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
38
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
39
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
40
+ }
41
+
42
+ /**
43
+ * Compute the OCP digest of a UTF-8 string.
44
+ * Note: the string is encoded as UTF-8 before hashing.
45
+ * The verifier must apply the same encoding to reproduce the digest.
46
+ *
47
+ * @param {string} text - UTF-8 string
48
+ * @returns {Promise<string>} SHA-256 digest as lowercase hex, no 0x prefix
49
+ */
50
+ async function hashString(text) {
51
+ const bytes = new TextEncoder().encode(text);
52
+ const hashBuffer = await crypto.subtle.digest("SHA-256", bytes);
53
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
54
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
55
+ }
56
+
57
+ // Usage example:
58
+ //
59
+ // const file = document.querySelector('input[type="file"]').files[0];
60
+ // const digest = await hashFile(file);
61
+ // // digest is ready to pass to record(bytes32) on-chain
62
+ // // or to include in an OCP proof envelope as commitment.digest
63
+
64
+ // Node.js export (if used in a build pipeline)
65
+ if (typeof module !== "undefined") {
66
+ module.exports = { hashFile, hashBytes, hashString };
67
+ }
@@ -0,0 +1,241 @@
1
+ #!/usr/bin/env node
2
+
3
+ // OCP Reference Verifier v2.1.0
4
+ // Implements: evm/event-log extraction rule
5
+ // Dependencies: zero — Node.js stdlib only (https, crypto, fs)
6
+ // Spec: docs/spec/appendix-evm-r.md
7
+
8
+ "use strict";
9
+
10
+ const fs = require("fs");
11
+ const crypto = require("crypto");
12
+ const https = require("https");
13
+
14
+ // RPC endpoints — no API key required
15
+ const RPC = {
16
+ "eip155:84532": "https://sepolia.base.org",
17
+ "eip155:8453": "https://mainnet.base.org",
18
+ "eip155:1": "https://cloudflare-eth.com",
19
+ "eip155:11155111": "https://rpc.sepolia.org",
20
+ };
21
+
22
+ // Network name to CAIP-2 chain ID (proof-format-v1 compatibility)
23
+ const NETWORK_TO_CHAIN_ID = {
24
+ "base-sepolia": "eip155:84532",
25
+ "base": "eip155:8453",
26
+ "mainnet": "eip155:1",
27
+ "homestead": "eip155:1",
28
+ "sepolia": "eip155:11155111",
29
+ };
30
+
31
+ // keccak256("Recorded(bytes32,address)")
32
+ // Confirmed from live Base Sepolia transaction logs
33
+ const KNOWN_EVENT_TOPIC = "0xdca60c2087041cbb12d9a57628c6cad28ecbd0437e47c7ab6c3aa6e162bf4497";
34
+
35
+ // Identity pipeline sentinel hash
36
+ // Canonical identity spec: ipfs://QmTst97dG8i9tFrutdetqMbVhSHqJGJaxMmPzWCcVVTWDU
37
+ // Confirmed independently against Dinamic.eth implementation — locked
38
+ const IDENTITY_PIPELINE_SENTINEL = "8116eec29078e8f57c07077d5e8080a35bde73036581df3abb93755d1b1a16ea";
39
+
40
+ function fail(message) {
41
+ console.error(`INVALID: ${message}`);
42
+ process.exit(1);
43
+ }
44
+
45
+ function log(message) {
46
+ console.log(message);
47
+ }
48
+
49
+ function rpcCall(url, method, params) {
50
+ return new Promise((resolve, reject) => {
51
+ const body = JSON.stringify({ jsonrpc: "2.0", id: 1, method, params });
52
+ const urlObj = new URL(url);
53
+ const options = {
54
+ hostname: urlObj.hostname,
55
+ path: urlObj.pathname,
56
+ method: "POST",
57
+ headers: {
58
+ "Content-Type": "application/json",
59
+ "Content-Length": Buffer.byteLength(body),
60
+ },
61
+ };
62
+ const req = https.request(options, (res) => {
63
+ let data = "";
64
+ res.on("data", (chunk) => { data += chunk; });
65
+ res.on("end", () => {
66
+ try {
67
+ const parsed = JSON.parse(data);
68
+ if (parsed.error) reject(new Error(`RPC error: ${parsed.error.message}`));
69
+ else resolve(parsed.result);
70
+ } catch (e) {
71
+ reject(new Error(`Failed to parse RPC response: ${data}`));
72
+ }
73
+ });
74
+ });
75
+ req.on("error", reject);
76
+ req.write(body);
77
+ req.end();
78
+ });
79
+ }
80
+
81
+ function normalizeHash(h) {
82
+ if (!h) return null;
83
+ return h.toLowerCase().startsWith("0x") ? h.toLowerCase() : "0x" + h.toLowerCase();
84
+ }
85
+
86
+ function stripPrefix(h) {
87
+ if (!h) return null;
88
+ return h.toLowerCase().startsWith("0x") ? h.slice(2).toLowerCase() : h.toLowerCase();
89
+ }
90
+
91
+ function applyExtractionRule(receipt, eventTopic) {
92
+ const extracted = new Set();
93
+ if (!receipt.logs || !Array.isArray(receipt.logs)) return extracted;
94
+ for (const log of receipt.logs) {
95
+ if (!log.topics || log.topics.length < 2) continue;
96
+ if (normalizeHash(log.topics[0]) !== normalizeHash(eventTopic)) continue;
97
+ const digest = stripPrefix(log.topics[1]);
98
+ if (digest) extracted.add(digest);
99
+ }
100
+ return extracted;
101
+ }
102
+
103
+ async function main() {
104
+ const [, , filePath, proofArg] = process.argv;
105
+
106
+ if (!filePath) {
107
+ console.error("Usage: ocp-verify <file> [proof.json]");
108
+ console.error("");
109
+ console.error("Environment:");
110
+ console.error(" OCP_RPC_URL=<url> Override RPC endpoint (optional)");
111
+ process.exit(1);
112
+ }
113
+
114
+ if (!fs.existsSync(filePath)) fail(`file not found: ${filePath}`);
115
+
116
+ const proofPath = proofArg || filePath.replace(/(\.[^/.]+)?$/, ".proof.json");
117
+ if (!fs.existsSync(proofPath)) fail(`proof not found: ${proofPath}`);
118
+
119
+ const fileBytes = fs.readFileSync(filePath);
120
+ let proof;
121
+ try {
122
+ proof = JSON.parse(fs.readFileSync(proofPath, "utf8"));
123
+ } catch {
124
+ fail("invalid JSON in proof file");
125
+ }
126
+
127
+ const isEnvelope = !!proof.ocp;
128
+ const isV1 = proof.version === "ocp-1";
129
+ if (!isEnvelope && !isV1) fail("unrecognized proof format — expected ocp-1 or envelope format");
130
+
131
+ let txHash, chainId, commitmentDigest, blockHash;
132
+
133
+ if (isV1) {
134
+ const requiredFields = ["version", "hash", "txHash", "network", "contract", "extractionRule"];
135
+ for (const field of requiredFields) {
136
+ if (!proof[field]) fail(`missing required field: ${field}`);
137
+ }
138
+ if (!/^0x[a-f0-9]{64}$/.test(proof.hash)) fail("invalid hash format");
139
+ if (!/^0x[a-fA-F0-9]{64}$/.test(proof.txHash)) fail("invalid txHash format");
140
+ if (!/^0x[a-fA-F0-9]{40}$/.test(proof.contract)) fail("invalid contract format");
141
+
142
+ txHash = proof.txHash;
143
+ chainId = NETWORK_TO_CHAIN_ID[proof.network];
144
+ commitmentDigest = stripPrefix(proof.hash);
145
+ blockHash = null;
146
+
147
+ if (!chainId) fail(`unknown network: ${proof.network} — add to NETWORK_TO_CHAIN_ID`);
148
+
149
+ } else {
150
+ if (!proof.chain?.id) fail("missing chain.id");
151
+ if (!proof.chain?.namespace) fail("missing chain.namespace");
152
+ if (!proof.commitment?.digest) fail("missing commitment.digest");
153
+ if (!proof.commitment?.hash_function) fail("missing commitment.hash_function");
154
+ if (!proof.commitment?.serialization) fail("missing commitment.serialization");
155
+ if (!proof.ledger_ref?.transaction_id) fail("missing ledger_ref.transaction_id");
156
+ if (!proof.ledger_ref?.block_hash) fail("missing ledger_ref.block_hash");
157
+ if (!proof.extraction?.rule_id) fail("missing extraction.rule_id");
158
+
159
+ if (proof.commitment.hash_function !== "sha2-256")
160
+ fail(`unsupported hash function: ${proof.commitment.hash_function}`);
161
+ if (proof.commitment.serialization !== "raw-bytes")
162
+ fail(`unsupported serialization: ${proof.commitment.serialization}`);
163
+ if (!proof.extraction.rule_id.startsWith("evm/"))
164
+ fail(`unsupported extraction rule namespace: ${proof.extraction.rule_id}`);
165
+
166
+ txHash = proof.ledger_ref.transaction_id;
167
+ chainId = proof.chain.id;
168
+ commitmentDigest = proof.commitment.digest.toLowerCase();
169
+ blockHash = proof.ledger_ref.block_hash;
170
+ }
171
+
172
+ // Step 1 — Recompute digest
173
+ const computedHash = crypto.createHash("sha256").update(fileBytes).digest("hex");
174
+ if (computedHash !== commitmentDigest) {
175
+ fail(`hash mismatch\n computed: ${computedHash}\n proof: ${commitmentDigest}`);
176
+ }
177
+ log(` hash MATCH ${computedHash}`);
178
+
179
+ // Step 2 — Resolve RPC
180
+ const rpcUrl = process.env.OCP_RPC_URL || RPC[chainId];
181
+ if (!rpcUrl) fail(`no RPC endpoint for chain ${chainId} — set OCP_RPC_URL`);
182
+
183
+ log(` chain ${chainId}`);
184
+ log(` rpc ${rpcUrl}`);
185
+ log(` tx ${txHash}`);
186
+
187
+ // Step 3 — Fetch receipt
188
+ let receipt;
189
+ try {
190
+ receipt = await rpcCall(rpcUrl, "eth_getTransactionReceipt", [txHash]);
191
+ } catch (e) {
192
+ fail(`RPC call failed: ${e.message}`);
193
+ }
194
+ if (!receipt) fail(`transaction not found: ${txHash}`);
195
+
196
+ // Step 4 — Confirm block hash (envelope only)
197
+ if (blockHash) {
198
+ const receiptBlockHash = normalizeHash(receipt.blockHash);
199
+ const expectedBlockHash = normalizeHash(blockHash);
200
+ if (receiptBlockHash !== expectedBlockHash) {
201
+ fail(`block hash mismatch\n receipt: ${receiptBlockHash}\n proof: ${expectedBlockHash}`);
202
+ }
203
+ log(` block MATCH ${receiptBlockHash}`);
204
+ }
205
+
206
+ // Step 5 — Apply extraction rule
207
+ const extracted = applyExtractionRule(receipt, KNOWN_EVENT_TOPIC);
208
+ if (extracted.size === 0) {
209
+ fail(
210
+ `no Recorded events found in transaction ${txHash}\n` +
211
+ ` event topic: ${KNOWN_EVENT_TOPIC}\n` +
212
+ ` logs found: ${receipt.logs?.length ?? 0}`
213
+ );
214
+ }
215
+ log(` logs found ${extracted.size} Recorded event(s)`);
216
+
217
+ // Step 6 — Confirm inclusion
218
+ if (!extracted.has(commitmentDigest)) {
219
+ fail(
220
+ `digest not found in transaction logs\n` +
221
+ ` looking for: ${commitmentDigest}\n` +
222
+ ` found: ${[...extracted].join(", ")}`
223
+ );
224
+ }
225
+ log(` digest MATCH ${commitmentDigest}`);
226
+
227
+ // Step 7 — Report finality
228
+ if (isEnvelope && proof.ledger_ref?.finality) {
229
+ const { depth, assertion_time_utc } = proof.ledger_ref.finality;
230
+ log(` finality depth=${depth} at ${assertion_time_utc}`);
231
+ if (depth < 3) log(` WARNING finality depth ${depth} is below recommended minimum (3)`);
232
+ }
233
+
234
+ log("");
235
+ log("VALID");
236
+ }
237
+
238
+ main().catch((e) => {
239
+ console.error(`ERROR: ${e.message}`);
240
+ process.exit(1);
241
+ });