ocp-verify 1.0.0 โ†’ 1.2.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 CHANGED
@@ -28,6 +28,18 @@ If one byte changes, verification fails โ€” across any system.
28
28
 
29
29
  ## ๐Ÿ”Œ Integrate in 2 Minutes
30
30
 
31
+ ### Install
32
+
33
+ ```bash
34
+ npm install -g ocp-verify
35
+ ```
36
+
37
+ Or use without installing:
38
+
39
+ ```bash
40
+ npx ocp-verify myfile.txt myfile.proof.json
41
+ ```
42
+
31
43
  ### 1) Commit (produce a proof)
32
44
 
33
45
  ```bash
@@ -301,6 +313,7 @@ The network only confirms that a commitment exists.
301
313
  - ๐Ÿค– AI Inference Attestation โ†’ `/docs/spec/appendix-ai-inference-attestation.md`
302
314
  - ๐Ÿงพ Proof Format โ†’ `/docs/spec/proof-format-v1.md`
303
315
  - ๐Ÿ” Examples โ†’ `/examples`
316
+ - โœ… Conformance Suite โ†’ `/conformance/run-conformance.sh`
304
317
  - โš™๏ธ Contracts โ†’ `/contracts`
305
318
  - ๐ŸŒ Live Demo โ†’ https://observation-commitment-protocol.vercel.app/
306
319
 
@@ -328,5 +341,59 @@ VALID
328
341
  v1.0.0 โ€” Cross-Chain Primitive
329
342
  Phase 1 complete โ€” proof envelope schema
330
343
  Phase 2 complete โ€” EVM reference implementation live
331
- Phase 3 complete โ€” Solana appendix, chain-agnostic claim validated
344
+ Phase 3 complete โ€” Solana devnet live, Gate 3 verified โ€” same observation committed on EVM and Solana
345
+ Phase 4 complete โ€” ocp-verify published to npm, zero dependencies
346
+ Phase 5 complete โ€” conformance suite, 11/11 tests pass
332
347
  First external contribution merged โ€” dinamic.eth / ERC-8004 (PR #1)
348
+
349
+
350
+ ---
351
+
352
+ ## Revocation Extension (v1.1.0)
353
+
354
+ OCP now includes an optional revocation layer. Original commitments are never deleted or mutated โ€” revocation is additive, represented as a new on-chain commitment referencing a prior digest.
355
+
356
+ ```js
357
+ const { verifyWithRevocation } = require('ocp-verify/reference-cli/revoke.js');
358
+
359
+ const status = await verifyWithRevocation(
360
+ digest,
361
+ asOfTimestamp,
362
+ 'eip155:84532'
363
+ );
364
+ // returns VALID | REVOKED | NOT_FOUND
365
+ ```
366
+
367
+ - ๐Ÿ“„ Revocation Spec โ†’ `/docs/spec/appendix-revocation-r.md`
368
+ - โ›“๏ธ Deployed Contract โ†’ `0x2fa07c85439850ff6C5688d926bDa6DaEe62Db15` (Base Sepolia)
369
+ - โœ… Revocation Conformance โ†’ `/conformance/revocation/run-revocation-conformance.sh`
370
+ - ๐Ÿ”‘ Event Topic โ†’ `0xc19951599a519bc320c0f352b2f92f315e8a2368bd0efb2e5dca3b1196e76112`
371
+
372
+ ---
373
+
374
+ ## Status
375
+
376
+ v1.1.0 โ€” Revocation Extension
377
+ Phase 6 complete โ€” additive revocation primitive, 8/8 conformance tests pass
378
+
379
+ ## Temporal Bounds Extension (v1.2.0)
380
+
381
+ OCP now includes an optional temporal bounds layer. Derives a finality-bounded existence proof from on-chain state โ€” no oracles, no trusted time servers.
382
+
383
+ ```js
384
+ const { getTemporalBound } = require('ocp-verify/reference-cli/temporal-bounds.js');
385
+
386
+ const result = await getTemporalBound(txHash, 'eip155:84532');
387
+ // result.upper_bound_iso โ€” observation existed no later than this time
388
+ // result.finalized โ€” whether safe finality depth has been reached
389
+ ```
390
+
391
+ - ๐Ÿ“„ Temporal Bounds Spec โ†’ `/docs/spec/appendix-temporal-bounds-r.md`
392
+ - โœ… Conformance โ†’ `/conformance/temporal-bounds/run-temporal-bounds-conformance.sh`
393
+
394
+ ---
395
+
396
+ ## Status
397
+
398
+ v1.2.0 โ€” Temporal Bounds Extension
399
+ Phase 7 complete โ€” finality-derived temporal bounds, 8/8 conformance tests pass
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ocp-verify",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Zero-dependency verifier for the Observation Commitment Protocol โ€” independently verify that a file was committed to a public blockchain",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -33,9 +33,9 @@
33
33
  "homepage": "https://github.com/damonzwicker/observation-commitment-protocol",
34
34
  "repository": {
35
35
  "type": "git",
36
- "url": "https://github.com/damonzwicker/observation-commitment-protocol.git"
36
+ "url": "git+https://github.com/damonzwicker/observation-commitment-protocol.git"
37
37
  },
38
38
  "bugs": {
39
39
  "url": "https://github.com/damonzwicker/observation-commitment-protocol/issues"
40
40
  }
41
- }
41
+ }
@@ -0,0 +1,148 @@
1
+ // revoke.js โ€” zero-dependency revocation helpers for OCP
2
+ // CommonJS, Node.js >=18
3
+ // Spec: docs/spec/appendix-revocation-r.md
4
+
5
+ "use strict";
6
+
7
+ const crypto = require("crypto");
8
+ const https = require("https");
9
+
10
+ // Deployed contract addresses
11
+ const REVOCATION_CONTRACTS = {
12
+ "eip155:84532": "0x2fa07c85439850ff6C5688d926bDa6DaEe62Db15",
13
+ };
14
+
15
+ // RPC endpoints โ€” no API key required
16
+ const RPC = {
17
+ "eip155:84532": "https://sepolia.base.org",
18
+ "eip155:8453": "https://mainnet.base.org",
19
+ };
20
+
21
+ // keccak256("RevocationCommitted(bytes32,bytes32,address,uint256)")
22
+ const REVOCATION_EVENT_TOPIC = "0xc19951599a519bc320c0f352b2f92f315e8a2368bd0efb2e5dca3b1196e76112";
23
+
24
+ // getRevocation(bytes32) selector
25
+ const GET_REVOCATION_SELECTOR = "0x8d4b2a4d";
26
+
27
+ function normalizeHexDigest(value, fieldName) {
28
+ if (typeof value !== "string") {
29
+ throw new TypeError(`${fieldName} must be a string`);
30
+ }
31
+ if (!/^0x[0-9a-fA-F]{64}$/.test(value)) {
32
+ throw new Error(`${fieldName} must be a 0x-prefixed 32-byte hex digest`);
33
+ }
34
+ return value.toLowerCase();
35
+ }
36
+
37
+ function stableJson(value) {
38
+ if (value === null || typeof value !== "object") {
39
+ return JSON.stringify(value);
40
+ }
41
+ if (Array.isArray(value)) {
42
+ return `[${value.map(stableJson).join(",")}]`;
43
+ }
44
+ const keys = Object.keys(value).sort();
45
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
46
+ }
47
+
48
+ function rpcCall(url, method, params) {
49
+ return new Promise((resolve, reject) => {
50
+ const body = JSON.stringify({ jsonrpc: "2.0", id: 1, method, params });
51
+ const urlObj = new URL(url);
52
+ const options = {
53
+ hostname: urlObj.hostname,
54
+ path: urlObj.pathname,
55
+ method: "POST",
56
+ headers: {
57
+ "Content-Type": "application/json",
58
+ "Content-Length": Buffer.byteLength(body),
59
+ },
60
+ };
61
+ const req = https.request(options, (res) => {
62
+ let data = "";
63
+ res.on("data", (chunk) => { data += chunk; });
64
+ res.on("end", () => {
65
+ try {
66
+ const parsed = JSON.parse(data);
67
+ if (parsed.error) reject(new Error(`RPC error: ${parsed.error.message}`));
68
+ else resolve(parsed.result);
69
+ } catch (e) {
70
+ reject(new Error(`Failed to parse RPC response: ${data}`));
71
+ }
72
+ });
73
+ });
74
+ req.on("error", reject);
75
+ req.write(body);
76
+ req.end();
77
+ });
78
+ }
79
+
80
+ function decodeRevocationRecord(hex) {
81
+ const data = hex.startsWith("0x") ? hex.slice(2) : hex;
82
+ const revocationDigest = "0x" + data.slice(0, 64);
83
+ const revoker = "0x" + data.slice(64, 128).slice(24);
84
+ const timestamp = parseInt(data.slice(128, 192), 16);
85
+ const exists = data.slice(192, 256) !== "0".repeat(64);
86
+ return { revocationDigest, revoker, timestamp, exists };
87
+ }
88
+
89
+ async function buildRevocationDigest(originalDigest, metadata = {}) {
90
+ const normalizedOriginalDigest = normalizeHexDigest(originalDigest, "originalDigest");
91
+ const payload = { originalDigest: normalizedOriginalDigest, ...metadata };
92
+ const encoded = stableJson(payload);
93
+ return `0x${crypto.createHash("sha256").update(encoded).digest("hex")}`;
94
+ }
95
+
96
+ async function checkRevocationStatus(originalDigest, chainId, rpcUrl, contractAddress) {
97
+ normalizeHexDigest(originalDigest, "originalDigest");
98
+
99
+ const resolvedRpc = rpcUrl || RPC[chainId];
100
+ const resolvedContract = contractAddress || REVOCATION_CONTRACTS[chainId];
101
+
102
+ if (!resolvedRpc) throw new Error(`no RPC endpoint for chain ${chainId} โ€” pass rpcUrl explicitly`);
103
+ if (!resolvedContract) throw new Error(`no contract address for chain ${chainId} โ€” pass contractAddress explicitly`);
104
+
105
+ const digestHex = originalDigest.slice(2).toLowerCase();
106
+ const calldata = GET_REVOCATION_SELECTOR + digestHex;
107
+
108
+ let result;
109
+ try {
110
+ result = await rpcCall(resolvedRpc, "eth_call", [
111
+ { to: resolvedContract, data: calldata },
112
+ "latest",
113
+ ]);
114
+ } catch (e) {
115
+ throw new Error(`RPC call failed: ${e.message}`);
116
+ }
117
+
118
+ if (!result || result === "0x") return { status: "NOT_FOUND", record: null };
119
+
120
+ const record = decodeRevocationRecord(result);
121
+ if (!record.exists) return { status: "NOT_FOUND", record: null };
122
+
123
+ return { status: "REVOKED", record };
124
+ }
125
+
126
+ async function verifyWithRevocation(originalDigest, asOfTimestamp, chainId, rpcUrl, contractAddress) {
127
+ normalizeHexDigest(originalDigest, "originalDigest");
128
+
129
+ if (typeof asOfTimestamp !== "number") {
130
+ throw new TypeError("asOfTimestamp must be a number (Unix seconds)");
131
+ }
132
+
133
+ const { status, record } = await checkRevocationStatus(
134
+ originalDigest, chainId, rpcUrl, contractAddress
135
+ );
136
+
137
+ if (status === "NOT_FOUND") return "NOT_FOUND";
138
+ if (record.timestamp <= asOfTimestamp) return "REVOKED";
139
+ return "VALID";
140
+ }
141
+
142
+ module.exports = {
143
+ buildRevocationDigest,
144
+ checkRevocationStatus,
145
+ verifyWithRevocation,
146
+ stableJson,
147
+ REVOCATION_EVENT_TOPIC,
148
+ };
@@ -0,0 +1,164 @@
1
+ // temporal-bounds.js โ€” zero-dependency temporal bound helper for OCP
2
+ // CommonJS, Node.js >=18
3
+ // Spec: docs/spec/appendix-temporal-bounds-r.md
4
+
5
+ "use strict";
6
+
7
+ const https = require("https");
8
+
9
+ // RPC endpoints โ€” no API key required
10
+ const RPC = {
11
+ "eip155:1": "https://cloudflare-eth.com",
12
+ "eip155:8453": "https://mainnet.base.org",
13
+ "eip155:84532": "https://sepolia.base.org",
14
+ "eip155:11155111": "https://rpc.sepolia.org",
15
+ };
16
+
17
+ // Finality table โ€” F(L) per network
18
+ // Source: docs/spec/appendix-temporal-bounds-r.md
19
+ const FINALITY = {
20
+ "eip155:1": {
21
+ safe_depth: 64,
22
+ finality_window_seconds: 720,
23
+ validator_influence_window: 12,
24
+ finality_model: "Casper FFG โ€” 2 epochs",
25
+ },
26
+ "eip155:8453": {
27
+ safe_depth: 32,
28
+ finality_window_seconds: 420,
29
+ validator_influence_window: 2,
30
+ finality_model: "L2 soft finality",
31
+ l1_chain_id: "eip155:1",
32
+ },
33
+ "eip155:84532": {
34
+ safe_depth: 32,
35
+ finality_window_seconds: 420,
36
+ validator_influence_window: 2,
37
+ finality_model: "L2 soft finality",
38
+ l1_chain_id: "eip155:11155111",
39
+ },
40
+ "eip155:11155111": {
41
+ safe_depth: 64,
42
+ finality_window_seconds: 720,
43
+ validator_influence_window: 12,
44
+ finality_model: "Casper FFG โ€” 2 epochs",
45
+ },
46
+ };
47
+
48
+ function rpcCall(url, method, params) {
49
+ return new Promise((resolve, reject) => {
50
+ const body = JSON.stringify({ jsonrpc: "2.0", id: 1, method, params });
51
+ const urlObj = new URL(url);
52
+ const options = {
53
+ hostname: urlObj.hostname,
54
+ path: urlObj.pathname,
55
+ method: "POST",
56
+ headers: {
57
+ "Content-Type": "application/json",
58
+ "Content-Length": Buffer.byteLength(body),
59
+ },
60
+ };
61
+ const req = https.request(options, (res) => {
62
+ let data = "";
63
+ res.on("data", (chunk) => { data += chunk; });
64
+ res.on("end", () => {
65
+ try {
66
+ const parsed = JSON.parse(data);
67
+ if (parsed.error) reject(new Error(`RPC error: ${parsed.error.message}`));
68
+ else resolve(parsed.result);
69
+ } catch (e) {
70
+ reject(new Error(`Failed to parse RPC response: ${data}`));
71
+ }
72
+ });
73
+ });
74
+ req.on("error", reject);
75
+ req.write(body);
76
+ req.end();
77
+ });
78
+ }
79
+
80
+ function toIso8601(unixSeconds) {
81
+ return new Date(unixSeconds * 1000).toISOString();
82
+ }
83
+
84
+ function normalizeHexDigest(value, fieldName) {
85
+ if (typeof value !== "string") {
86
+ throw new TypeError(`${fieldName} must be a string`);
87
+ }
88
+ if (!/^0x[0-9a-fA-F]{64}$/.test(value)) {
89
+ throw new Error(`${fieldName} must be a 0x-prefixed 32-byte hex digest`);
90
+ }
91
+ return value.toLowerCase();
92
+ }
93
+
94
+ async function getTemporalBound(txHash, chainId, rpcUrl) {
95
+ if (!txHash || !/^0x[0-9a-fA-F]{64}$/.test(txHash)) {
96
+ throw new Error("txHash must be a 0x-prefixed 32-byte hex string");
97
+ }
98
+
99
+ const finality = FINALITY[chainId];
100
+ if (!finality) {
101
+ throw new Error(`unsupported chainId: ${chainId} โ€” add to FINALITY table`);
102
+ }
103
+
104
+ const resolvedRpc = rpcUrl || RPC[chainId];
105
+ if (!resolvedRpc) {
106
+ throw new Error(`no RPC endpoint for chain ${chainId} โ€” pass rpcUrl explicitly`);
107
+ }
108
+
109
+ // Step 1 โ€” fetch transaction receipt to get block number
110
+ let receipt;
111
+ try {
112
+ receipt = await rpcCall(resolvedRpc, "eth_getTransactionReceipt", [txHash]);
113
+ } catch (e) {
114
+ throw new Error(`RPC call failed: ${e.message}`);
115
+ }
116
+ if (!receipt) throw new Error(`transaction not found: ${txHash}`);
117
+
118
+ const blockNumber = parseInt(receipt.blockNumber, 16);
119
+
120
+ // Step 2 โ€” fetch block to get timestamp
121
+ let block;
122
+ try {
123
+ block = await rpcCall(resolvedRpc, "eth_getBlockByNumber", [receipt.blockNumber, false]);
124
+ } catch (e) {
125
+ throw new Error(`RPC call failed fetching block: ${e.message}`);
126
+ }
127
+ if (!block) throw new Error(`block not found: ${receipt.blockNumber}`);
128
+
129
+ const blockTimestamp = parseInt(block.timestamp, 16);
130
+
131
+ // Step 3 โ€” fetch current block number to compute finality depth
132
+ let currentBlockHex;
133
+ try {
134
+ currentBlockHex = await rpcCall(resolvedRpc, "eth_blockNumber", []);
135
+ } catch (e) {
136
+ throw new Error(`RPC call failed fetching current block: ${e.message}`);
137
+ }
138
+ const currentBlock = parseInt(currentBlockHex, 16);
139
+ const depth = currentBlock - blockNumber;
140
+ const finalized = depth >= finality.safe_depth;
141
+
142
+ // Step 4 โ€” compute upper bound
143
+ const upperBound = blockTimestamp + finality.validator_influence_window;
144
+
145
+ return {
146
+ tx_hash: txHash,
147
+ chain_id: chainId,
148
+ block_number: blockNumber,
149
+ block_timestamp: blockTimestamp,
150
+ block_timestamp_iso: toIso8601(blockTimestamp),
151
+ finality_depth_confirmed: depth,
152
+ finality_depth_required: finality.safe_depth,
153
+ finality_model: finality.finality_model,
154
+ finalized,
155
+ validator_influence_window: finality.validator_influence_window,
156
+ upper_bound: upperBound,
157
+ upper_bound_iso: toIso8601(upperBound),
158
+ };
159
+ }
160
+
161
+ module.exports = {
162
+ getTemporalBound,
163
+ FINALITY,
164
+ };