ocp-verify 1.1.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 +22 -0
- package/package.json +1 -1
- package/reference-cli/temporal-bounds.js +164 -0
package/README.md
CHANGED
|
@@ -375,3 +375,25 @@ const status = await verifyWithRevocation(
|
|
|
375
375
|
|
|
376
376
|
v1.1.0 — Revocation Extension
|
|
377
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.
|
|
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",
|
|
@@ -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
|
+
};
|