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 +20 -0
- package/README.md +332 -0
- package/package.json +41 -0
- package/reference-cli/README.md +82 -0
- package/reference-cli/commit.js +37 -0
- package/reference-cli/hash-browser.js +67 -0
- package/reference-cli/verify.js +241 -0
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
|
+
});
|