ocp-verify 1.2.0 → 2.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/CHANGELOG.md +30 -0
- package/README.md +89 -365
- package/package.json +32 -31
- package/src/hash.js +205 -0
- package/src/index.js +42 -0
- package/src/index.mjs +26 -0
- package/src/normalize.js +89 -0
- package/src/profiles.js +78 -0
- package/src/verify.js +141 -0
- package/LICENSE +0 -20
- package/reference-cli/README.md +0 -82
- package/reference-cli/commit.js +0 -37
- package/reference-cli/hash-browser.js +0 -67
- package/reference-cli/revoke.js +0 -148
- package/reference-cli/temporal-bounds.js +0 -164
- package/reference-cli/verify.js +0 -241
package/src/hash.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CC0-1.0
|
|
2
|
+
// ERC-8281 Hash Function Registry implementations — dependency-free.
|
|
3
|
+
//
|
|
4
|
+
// sha2-256: node:crypto (built-in)
|
|
5
|
+
// keccak-256: pure JS (original Keccak padding 0x01, rate 1088), BigInt lanes
|
|
6
|
+
// blake2b-256: pure JS RFC 7693 BLAKE2b with digest_length = 32.
|
|
7
|
+
// NOTE: this is the PARAMETERIZED form — digest length is part of
|
|
8
|
+
// the parameter block XORed into the IV. It is NOT a truncation
|
|
9
|
+
// of BLAKE2b-512 and produces a different digest, per the
|
|
10
|
+
// Hash Function Registry note in ERC-8281.
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const { createHash } = require('node:crypto');
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Keccak-256
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
const KECCAK_RC = [
|
|
21
|
+
0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n,
|
|
22
|
+
0x000000000000808bn, 0x0000000080000001n, 0x8000000080008081n, 0x8000000000008009n,
|
|
23
|
+
0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
|
|
24
|
+
0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n,
|
|
25
|
+
0x8000000000008002n, 0x8000000000000080n, 0x000000000000800an, 0x800000008000000an,
|
|
26
|
+
0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
|
|
27
|
+
];
|
|
28
|
+
const KECCAK_ROT = [
|
|
29
|
+
[0n, 36n, 3n, 41n, 18n],
|
|
30
|
+
[1n, 44n, 10n, 45n, 2n],
|
|
31
|
+
[62n, 6n, 43n, 15n, 61n],
|
|
32
|
+
[28n, 55n, 25n, 21n, 56n],
|
|
33
|
+
[27n, 20n, 39n, 8n, 14n],
|
|
34
|
+
];
|
|
35
|
+
const M64 = (1n << 64n) - 1n;
|
|
36
|
+
const rol64 = (x, n) => (n === 0n ? x : (((x << n) | (x >> (64n - n))) & M64));
|
|
37
|
+
|
|
38
|
+
function keccakF(A) {
|
|
39
|
+
for (let r = 0; r < 24; r++) {
|
|
40
|
+
const C = [], D = [];
|
|
41
|
+
for (let x = 0; x < 5; x++) C[x] = A[x][0] ^ A[x][1] ^ A[x][2] ^ A[x][3] ^ A[x][4];
|
|
42
|
+
for (let x = 0; x < 5; x++) D[x] = C[(x + 4) % 5] ^ rol64(C[(x + 1) % 5], 1n);
|
|
43
|
+
for (let x = 0; x < 5; x++) for (let y = 0; y < 5; y++) A[x][y] ^= D[x];
|
|
44
|
+
const B = [[], [], [], [], []];
|
|
45
|
+
for (let x = 0; x < 5; x++)
|
|
46
|
+
for (let y = 0; y < 5; y++) B[y][(2 * x + 3 * y) % 5] = rol64(A[x][y], KECCAK_ROT[x][y]);
|
|
47
|
+
for (let x = 0; x < 5; x++)
|
|
48
|
+
for (let y = 0; y < 5; y++) A[x][y] = B[x][y] ^ ((~B[(x + 1) % 5][y] & M64) & B[(x + 2) % 5][y]);
|
|
49
|
+
A[0][0] ^= KECCAK_RC[r];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function keccak256(bytes) {
|
|
54
|
+
const rate = 136;
|
|
55
|
+
const padLen = rate - (bytes.length % rate);
|
|
56
|
+
const p = new Uint8Array(bytes.length + padLen);
|
|
57
|
+
p.set(bytes);
|
|
58
|
+
p[bytes.length] = 0x01;
|
|
59
|
+
p[p.length - 1] |= 0x80;
|
|
60
|
+
const A = Array.from({ length: 5 }, () => [0n, 0n, 0n, 0n, 0n]);
|
|
61
|
+
for (let off = 0; off < p.length; off += rate) {
|
|
62
|
+
for (let i = 0; i < rate / 8; i++) {
|
|
63
|
+
let lane = 0n;
|
|
64
|
+
for (let b = 7; b >= 0; b--) lane = (lane << 8n) | BigInt(p[off + i * 8 + b]);
|
|
65
|
+
A[i % 5][(i / 5) | 0] ^= lane;
|
|
66
|
+
}
|
|
67
|
+
keccakF(A);
|
|
68
|
+
}
|
|
69
|
+
const out = new Uint8Array(32);
|
|
70
|
+
for (let i = 0; i < 4; i++) {
|
|
71
|
+
let lane = A[i % 5][(i / 5) | 0];
|
|
72
|
+
for (let b = 0; b < 8; b++) { out[i * 8 + b] = Number(lane & 0xffn); lane >>= 8n; }
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// BLAKE2b-256 (parameterized, RFC 7693)
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
const B2B_IV = [
|
|
82
|
+
0x6a09e667f3bcc908n, 0xbb67ae8584caa73bn, 0x3c6ef372fe94f82bn, 0xa54ff53a5f1d36f1n,
|
|
83
|
+
0x510e527fade682d1n, 0x9b05688c2b3e6c1fn, 0x1f83d9abfb41bd6bn, 0x5be0cd19137e2179n,
|
|
84
|
+
];
|
|
85
|
+
const B2B_SIGMA = [
|
|
86
|
+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
87
|
+
[14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
|
|
88
|
+
[11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4],
|
|
89
|
+
[7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8],
|
|
90
|
+
[9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13],
|
|
91
|
+
[2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9],
|
|
92
|
+
[12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11],
|
|
93
|
+
[13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10],
|
|
94
|
+
[6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5],
|
|
95
|
+
[10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0],
|
|
96
|
+
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
97
|
+
[14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3],
|
|
98
|
+
];
|
|
99
|
+
const ror64 = (x, n) => ((x >> n) | (x << (64n - n))) & M64;
|
|
100
|
+
|
|
101
|
+
function b2bCompress(h, block, t, last) {
|
|
102
|
+
const m = [];
|
|
103
|
+
for (let i = 0; i < 16; i++) {
|
|
104
|
+
let w = 0n;
|
|
105
|
+
for (let b = 7; b >= 0; b--) w = (w << 8n) | BigInt(block[i * 8 + b]);
|
|
106
|
+
m[i] = w;
|
|
107
|
+
}
|
|
108
|
+
const v = h.concat(B2B_IV.slice());
|
|
109
|
+
v[12] ^= t & M64;
|
|
110
|
+
v[13] ^= 0n; // high word of t — inputs here never exceed 2^64 bytes
|
|
111
|
+
if (last) v[14] ^= M64;
|
|
112
|
+
const G = (a, b, c, d, x, y) => {
|
|
113
|
+
v[a] = (v[a] + v[b] + x) & M64; v[d] = ror64(v[d] ^ v[a], 32n);
|
|
114
|
+
v[c] = (v[c] + v[d]) & M64; v[b] = ror64(v[b] ^ v[c], 24n);
|
|
115
|
+
v[a] = (v[a] + v[b] + y) & M64; v[d] = ror64(v[d] ^ v[a], 16n);
|
|
116
|
+
v[c] = (v[c] + v[d]) & M64; v[b] = ror64(v[b] ^ v[c], 63n);
|
|
117
|
+
};
|
|
118
|
+
for (let r = 0; r < 12; r++) {
|
|
119
|
+
const s = B2B_SIGMA[r];
|
|
120
|
+
G(0, 4, 8, 12, m[s[0]], m[s[1]]);
|
|
121
|
+
G(1, 5, 9, 13, m[s[2]], m[s[3]]);
|
|
122
|
+
G(2, 6, 10, 14, m[s[4]], m[s[5]]);
|
|
123
|
+
G(3, 7, 11, 15, m[s[6]], m[s[7]]);
|
|
124
|
+
G(0, 5, 10, 15, m[s[8]], m[s[9]]);
|
|
125
|
+
G(1, 6, 11, 12, m[s[10]], m[s[11]]);
|
|
126
|
+
G(2, 7, 8, 13, m[s[12]], m[s[13]]);
|
|
127
|
+
G(3, 4, 9, 14, m[s[14]], m[s[15]]);
|
|
128
|
+
}
|
|
129
|
+
for (let i = 0; i < 8; i++) h[i] ^= v[i] ^ v[i + 8];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function blake2b256(bytes) {
|
|
133
|
+
const outLen = 32;
|
|
134
|
+
const h = B2B_IV.slice();
|
|
135
|
+
h[0] ^= 0x01010000n ^ BigInt(outLen); // param block: digest_length=32, fanout=1, depth=1
|
|
136
|
+
let t = 0n;
|
|
137
|
+
let i = 0;
|
|
138
|
+
while (bytes.length - i > 128) {
|
|
139
|
+
t += 128n;
|
|
140
|
+
b2bCompress(h, bytes.subarray(i, i + 128), t, false);
|
|
141
|
+
i += 128;
|
|
142
|
+
}
|
|
143
|
+
const lastBlock = new Uint8Array(128);
|
|
144
|
+
lastBlock.set(bytes.subarray(i));
|
|
145
|
+
t += BigInt(bytes.length - i);
|
|
146
|
+
b2bCompress(h, lastBlock, t, true);
|
|
147
|
+
const out = new Uint8Array(outLen);
|
|
148
|
+
for (let w = 0; w < 4; w++) {
|
|
149
|
+
let word = h[w];
|
|
150
|
+
for (let b = 0; b < 8; b++) { out[w * 8 + b] = Number(word & 0xffn); word >>= 8n; }
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Registry dispatch + self-test
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
const sha256 = (bytes) => new Uint8Array(createHash('sha256').update(bytes).digest());
|
|
160
|
+
|
|
161
|
+
const ALLOWED_HASH_FUNCTIONS = Object.freeze(['sha2-256', 'keccak-256', 'blake2b-256']);
|
|
162
|
+
|
|
163
|
+
/** Compute the digest for a registry hash function. Returns null for
|
|
164
|
+
* identifiers not in the allowed set (the caller MUST reject). */
|
|
165
|
+
function computeDigest(hashFunction, bytes) {
|
|
166
|
+
switch (hashFunction) {
|
|
167
|
+
case 'sha2-256': return sha256(bytes);
|
|
168
|
+
case 'keccak-256': return keccak256(bytes);
|
|
169
|
+
case 'blake2b-256': return blake2b256(bytes);
|
|
170
|
+
default: return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const toHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
|
|
175
|
+
|
|
176
|
+
/** Validate all three implementations against known test vectors.
|
|
177
|
+
* Throws on any mismatch. Run automatically by the test suite. */
|
|
178
|
+
function selfTest() {
|
|
179
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
180
|
+
const checks = [
|
|
181
|
+
[sha256(enc('abc')),
|
|
182
|
+
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
|
|
183
|
+
'sha2-256("abc") — FIPS 180-4 vector'],
|
|
184
|
+
[keccak256(enc('')),
|
|
185
|
+
'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470',
|
|
186
|
+
'keccak-256("") — canonical empty-string vector'],
|
|
187
|
+
[keccak256(enc('Transfer(address,address,uint256)')),
|
|
188
|
+
'ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef',
|
|
189
|
+
'keccak-256(ERC-20 Transfer signature) — canonical topic-0'],
|
|
190
|
+
[blake2b256(enc('abc')),
|
|
191
|
+
'bddd813c634239723171ef3fee98579b94964e3bb1cb3e427262c8c068d52319',
|
|
192
|
+
'blake2b-256("abc") — parameterized digest_length=32'],
|
|
193
|
+
[blake2b256(new Uint8Array(0)),
|
|
194
|
+
'0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8',
|
|
195
|
+
'blake2b-256("") — parameterized digest_length=32'],
|
|
196
|
+
];
|
|
197
|
+
for (const [got, want, name] of checks) {
|
|
198
|
+
if (toHex(got) !== want) {
|
|
199
|
+
throw new Error(`hash self-test FAILED: ${name}\n got ${toHex(got)}\n want ${want}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = { sha256, keccak256, blake2b256, computeDigest, ALLOWED_HASH_FUNCTIONS, selfTest };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CC0-1.0
|
|
2
|
+
// ocp-verify — ERC-8281 Observation Commitment Protocol reference verifier.
|
|
3
|
+
// CommonJS entry point. ESM consumers are served by src/index.mjs.
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { verify, httpProvider } = require('./verify.js');
|
|
8
|
+
const { PROFILES, DEFAULT_PROFILE, resolveProfile } = require('./profiles.js');
|
|
9
|
+
const {
|
|
10
|
+
sha256, keccak256, blake2b256, computeDigest, ALLOWED_HASH_FUNCTIONS, selfTest,
|
|
11
|
+
} = require('./hash.js');
|
|
12
|
+
const { ENVELOPE_VERSION, validateEnvelope, isValidEip55 } = require('./normalize.js');
|
|
13
|
+
|
|
14
|
+
// Verified constants (ERC-8281 + Extraction Profiles extension)
|
|
15
|
+
const TOPIC0_RECORDED = PROFILES.recorded.topic0;
|
|
16
|
+
const TOPIC0_ERC8263_ANCHORPROOF = PROFILES['erc8263-anchorproof'].topic0;
|
|
17
|
+
const ERC165_INTERFACE_ID = '0xb5c645bd'; // bytes4(keccak256("record(bytes32)"))
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
// core
|
|
21
|
+
verify,
|
|
22
|
+
httpProvider,
|
|
23
|
+
// profiles
|
|
24
|
+
PROFILES,
|
|
25
|
+
DEFAULT_PROFILE,
|
|
26
|
+
resolveProfile,
|
|
27
|
+
// hashes
|
|
28
|
+
sha256,
|
|
29
|
+
keccak256,
|
|
30
|
+
blake2b256,
|
|
31
|
+
computeDigest,
|
|
32
|
+
ALLOWED_HASH_FUNCTIONS,
|
|
33
|
+
selfTest,
|
|
34
|
+
// normalization
|
|
35
|
+
ENVELOPE_VERSION,
|
|
36
|
+
validateEnvelope,
|
|
37
|
+
isValidEip55,
|
|
38
|
+
// constants
|
|
39
|
+
TOPIC0_RECORDED,
|
|
40
|
+
TOPIC0_ERC8263_ANCHORPROOF,
|
|
41
|
+
ERC165_INTERFACE_ID,
|
|
42
|
+
};
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CC0-1.0
|
|
2
|
+
// ocp-verify — ESM entry point (re-exports the CommonJS core).
|
|
3
|
+
|
|
4
|
+
import cjs from './index.js';
|
|
5
|
+
|
|
6
|
+
export const {
|
|
7
|
+
verify,
|
|
8
|
+
httpProvider,
|
|
9
|
+
PROFILES,
|
|
10
|
+
DEFAULT_PROFILE,
|
|
11
|
+
resolveProfile,
|
|
12
|
+
sha256,
|
|
13
|
+
keccak256,
|
|
14
|
+
blake2b256,
|
|
15
|
+
computeDigest,
|
|
16
|
+
ALLOWED_HASH_FUNCTIONS,
|
|
17
|
+
selfTest,
|
|
18
|
+
ENVELOPE_VERSION,
|
|
19
|
+
validateEnvelope,
|
|
20
|
+
isValidEip55,
|
|
21
|
+
TOPIC0_RECORDED,
|
|
22
|
+
TOPIC0_ERC8263_ANCHORPROOF,
|
|
23
|
+
ERC165_INTERFACE_ID,
|
|
24
|
+
} = cjs;
|
|
25
|
+
|
|
26
|
+
export default cjs;
|
package/src/normalize.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CC0-1.0
|
|
2
|
+
// ERC-8281 envelope field normalization and validation.
|
|
3
|
+
//
|
|
4
|
+
// Implements the Field Normalization Rules of the Proof Envelope Schema:
|
|
5
|
+
// all comparisons are over decoded values (bytes / integers), never raw
|
|
6
|
+
// strings; addresses MUST pass EIP-55 checksum validation; unknown
|
|
7
|
+
// additional fields MUST be ignored.
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { keccak256, ALLOWED_HASH_FUNCTIONS } = require('./hash.js');
|
|
12
|
+
|
|
13
|
+
const ENVELOPE_VERSION = 'erc8281/1';
|
|
14
|
+
|
|
15
|
+
// --- encoding helpers -------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const toHex = (bytes) => '0x' + Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
18
|
+
const hexToBytes = (s) => Uint8Array.from(s.slice(2).match(/.{2}/g) ?? [], (h) => parseInt(h, 16));
|
|
19
|
+
const bytesEq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
|
|
20
|
+
|
|
21
|
+
const isHex32 = (s) => typeof s === 'string' && /^0x[0-9a-f]{64}$/.test(s);
|
|
22
|
+
const isDecimal = (s) => typeof s === 'string' && /^(0|[1-9][0-9]*)$/.test(s);
|
|
23
|
+
const isAddressShape = (s) => typeof s === 'string' && /^0x[0-9a-fA-F]{40}$/.test(s);
|
|
24
|
+
|
|
25
|
+
/** Decode a 0x-prefixed checksummed address to its 20-byte value. */
|
|
26
|
+
const addressBytes = (address) => hexToBytes('0x' + address.slice(2).toLowerCase());
|
|
27
|
+
|
|
28
|
+
/** JSON-RPC quantity (0x-hex) or envelope decimal string → BigInt. */
|
|
29
|
+
const quantityToBigInt = (q) => BigInt(q);
|
|
30
|
+
const decimalToBigInt = (d) => BigInt(d);
|
|
31
|
+
|
|
32
|
+
// --- EIP-55 -----------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* EIP-55 checksum validation: each hex letter is uppercase iff the
|
|
36
|
+
* corresponding nibble of keccak256(lowercase-ascii-address-without-0x)
|
|
37
|
+
* is >= 8. Addresses that fail validation MUST be rejected.
|
|
38
|
+
*/
|
|
39
|
+
function isValidEip55(address) {
|
|
40
|
+
if (!isAddressShape(address)) return false;
|
|
41
|
+
const lower = address.slice(2).toLowerCase();
|
|
42
|
+
const hash = toHex(keccak256(new TextEncoder().encode(lower))).slice(2);
|
|
43
|
+
for (let i = 0; i < 40; i++) {
|
|
44
|
+
const c = address[2 + i];
|
|
45
|
+
if (!/[a-fA-F]/.test(c)) continue;
|
|
46
|
+
const upper = parseInt(hash[i], 16) >= 8;
|
|
47
|
+
if (upper !== (c === c.toUpperCase())) return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- envelope validation -----------------------------------------------------
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate envelope schema and normalization rules.
|
|
56
|
+
* Returns null when valid, otherwise a rejection reason string.
|
|
57
|
+
* Unknown additional fields are ignored, per the spec.
|
|
58
|
+
* (The extraction_profile registry check lives in profiles.js.)
|
|
59
|
+
*/
|
|
60
|
+
function validateEnvelope(env) {
|
|
61
|
+
if (typeof env !== 'object' || env === null) return 'envelope_not_object';
|
|
62
|
+
if (env.version !== ENVELOPE_VERSION) return 'version_not_supported'; // exact, case-sensitive
|
|
63
|
+
if (!isHex32(env.digest)) return 'digest_format_invalid';
|
|
64
|
+
if (!ALLOWED_HASH_FUNCTIONS.includes(env.hash_function)) return 'hash_function_not_allowed';
|
|
65
|
+
if (!isDecimal(env.chain_id)) return 'chain_id_format_invalid';
|
|
66
|
+
if (!isAddressShape(env.contract)) return 'contract_format_invalid';
|
|
67
|
+
if (!isValidEip55(env.contract)) return 'eip55_checksum_invalid';
|
|
68
|
+
if (!isHex32(env.tx_hash)) return 'tx_hash_format_invalid';
|
|
69
|
+
if (!isDecimal(env.block_number)) return 'block_number_format_invalid';
|
|
70
|
+
if (!isDecimal(env.receipt_log_position)) return 'log_position_format_invalid';
|
|
71
|
+
if (!isAddressShape(env.committer)) return 'committer_format_invalid';
|
|
72
|
+
if (!isValidEip55(env.committer)) return 'eip55_checksum_invalid';
|
|
73
|
+
if (env.block_hash !== undefined && !isHex32(env.block_hash)) return 'block_hash_format_invalid';
|
|
74
|
+
if (env.extraction_profile !== undefined && typeof env.extraction_profile !== 'string')
|
|
75
|
+
return 'profile_format_invalid';
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
ENVELOPE_VERSION,
|
|
81
|
+
validateEnvelope,
|
|
82
|
+
isValidEip55,
|
|
83
|
+
toHex,
|
|
84
|
+
hexToBytes,
|
|
85
|
+
bytesEq,
|
|
86
|
+
addressBytes,
|
|
87
|
+
quantityToBigInt,
|
|
88
|
+
decimalToBigInt,
|
|
89
|
+
};
|
package/src/profiles.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CC0-1.0
|
|
2
|
+
// ERC-8281 Extraction Profile Registry.
|
|
3
|
+
//
|
|
4
|
+
// A profile is a fixed, spec-registered parameterization of the extraction
|
|
5
|
+
// rule. Profiles are defined only in ERC-8281 or successor ERCs; proof
|
|
6
|
+
// envelopes MUST NOT carry inline extraction parameters. No two registered
|
|
7
|
+
// profiles share a topic-0 value, so any given log can satisfy at most one
|
|
8
|
+
// registered profile.
|
|
9
|
+
//
|
|
10
|
+
// All topic-0 literals below are computationally verified against their
|
|
11
|
+
// signature strings (see test suite, "constants" vector).
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_PROFILE = 'recorded';
|
|
16
|
+
|
|
17
|
+
const PROFILES = Object.freeze({
|
|
18
|
+
/**
|
|
19
|
+
* Default profile — the canonical ERC-8281 event.
|
|
20
|
+
* An envelope that omits `extraction_profile` MUST be processed under
|
|
21
|
+
* this profile.
|
|
22
|
+
*/
|
|
23
|
+
recorded: Object.freeze({
|
|
24
|
+
id: 'recorded',
|
|
25
|
+
eventSignature: 'Recorded(bytes32,address)',
|
|
26
|
+
topic0: '0xdca60c2087041cbb12d9a57628c6cad28ecbd0437e47c7ab6c3aa6e162bf4497',
|
|
27
|
+
topicCount: 3,
|
|
28
|
+
digestTopicIndex: 1,
|
|
29
|
+
committerTopicIndex: 2,
|
|
30
|
+
// No profile-specific assertions.
|
|
31
|
+
assertions: Object.freeze([]),
|
|
32
|
+
}),
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* ERC-8263 TruthAnchor profile.
|
|
36
|
+
* Event: AnchorProof(uint8,bytes32,bytes32,address,bytes)
|
|
37
|
+
* topics[1] = agentId (NOT authenticated by this procedure)
|
|
38
|
+
* topics[2] = proofHash (the committed digest)
|
|
39
|
+
* topics[3] = operator (maps to the envelope `committer` field)
|
|
40
|
+
* The data field contains ABI-encoded (agentIdScheme, aux) and is ignored.
|
|
41
|
+
*
|
|
42
|
+
* Verification under this profile authenticates ONLY the digest and the
|
|
43
|
+
* committer. It does NOT authenticate agentId, agentIdScheme, or aux, and
|
|
44
|
+
* establishes no binding between the digest and an agent identity —
|
|
45
|
+
* agentId resolution is defined by ERC-8263/ERC-8004 and is out of scope.
|
|
46
|
+
*/
|
|
47
|
+
'erc8263-anchorproof': Object.freeze({
|
|
48
|
+
id: 'erc8263-anchorproof',
|
|
49
|
+
eventSignature: 'AnchorProof(uint8,bytes32,bytes32,address,bytes)',
|
|
50
|
+
topic0: '0x9fe832d83a52f83bd7d54181e4cc7ff8b4e227cc1d3a0144376894b5df6c23cc',
|
|
51
|
+
topicCount: 4,
|
|
52
|
+
digestTopicIndex: 2,
|
|
53
|
+
committerTopicIndex: 3,
|
|
54
|
+
// Mirrors the ERC-8263 canonical-form guard: proofHash != bytes32(0).
|
|
55
|
+
assertions: Object.freeze([
|
|
56
|
+
{
|
|
57
|
+
name: 'nonzero_digest',
|
|
58
|
+
reason: 'profile_assertion_failed',
|
|
59
|
+
check: (ctx) => !ctx.onChainDigest.every((b) => b === 0),
|
|
60
|
+
},
|
|
61
|
+
]),
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the profile declared by an envelope.
|
|
67
|
+
* Returns { profile } on success, { error } on rejection.
|
|
68
|
+
* Absence of the field and the explicit value "recorded" are equivalent.
|
|
69
|
+
*/
|
|
70
|
+
function resolveProfile(envelope) {
|
|
71
|
+
const id = envelope.extraction_profile === undefined ? DEFAULT_PROFILE : envelope.extraction_profile;
|
|
72
|
+
if (typeof id !== 'string') return { error: 'profile_format_invalid' };
|
|
73
|
+
const profile = Object.prototype.hasOwnProperty.call(PROFILES, id) ? PROFILES[id] : undefined;
|
|
74
|
+
if (!profile) return { error: 'profile_not_registered' };
|
|
75
|
+
return { profile };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { PROFILES, DEFAULT_PROFILE, resolveProfile };
|
package/src/verify.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// SPDX-License-Identifier: CC0-1.0
|
|
2
|
+
// ERC-8281 verification invariant, profile-parameterized.
|
|
3
|
+
//
|
|
4
|
+
// 1. Recompute — hash the supplied observation bytes with the declared hash_function
|
|
5
|
+
// 2. Compare — H_prime == digest
|
|
6
|
+
// 3. Confirm inclusion — extraction rule under the declared profile:
|
|
7
|
+
// chain check → receipt fetch (status==1) → log selection by
|
|
8
|
+
// receipt_log_position (array position, NOT JSON-RPC logIndex) →
|
|
9
|
+
// address assertion → topic-0 + topic-count assertions →
|
|
10
|
+
// digest extraction → profile-specific assertions
|
|
11
|
+
// 4. Confirm committer — committer topic == envelope committer
|
|
12
|
+
// 5. Confirm block — receipt.blockNumber == envelope block_number
|
|
13
|
+
// plus block_hash, MUST-when-present, against the receipt's blockHash.
|
|
14
|
+
//
|
|
15
|
+
// All failures return { valid: false, reason } — partial verification is
|
|
16
|
+
// never reported as success.
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { computeDigest } = require('./hash.js');
|
|
21
|
+
const { resolveProfile } = require('./profiles.js');
|
|
22
|
+
const {
|
|
23
|
+
validateEnvelope,
|
|
24
|
+
hexToBytes,
|
|
25
|
+
bytesEq,
|
|
26
|
+
addressBytes,
|
|
27
|
+
quantityToBigInt,
|
|
28
|
+
decimalToBigInt,
|
|
29
|
+
} = require('./normalize.js');
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Verify a proof envelope against supplied observation bytes.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} envelope - ERC-8281 proof envelope (erc8281/1)
|
|
35
|
+
* @param {Uint8Array} observationBytes - the observation, supplied out-of-band;
|
|
36
|
+
* verification is performed over exactly these bytes as provided
|
|
37
|
+
* @param {object} provider - RPC access:
|
|
38
|
+
* { eth_chainId(): Promise<string>, // 0x-hex quantity
|
|
39
|
+
* eth_getTransactionReceipt(txHash): Promise<object|null> }
|
|
40
|
+
* @returns {Promise<{valid: true} | {valid: false, reason: string}>}
|
|
41
|
+
*/
|
|
42
|
+
async function verify(envelope, observationBytes, provider) {
|
|
43
|
+
const reject = (reason) => ({ valid: false, reason });
|
|
44
|
+
|
|
45
|
+
// Envelope schema + normalization (version, formats, registry, EIP-55)
|
|
46
|
+
const schemaError = validateEnvelope(envelope);
|
|
47
|
+
if (schemaError) return reject(schemaError);
|
|
48
|
+
|
|
49
|
+
// Profile resolution (closed registry; absent field == 'recorded')
|
|
50
|
+
const resolved = resolveProfile(envelope);
|
|
51
|
+
if (resolved.error) return reject(resolved.error);
|
|
52
|
+
const P = resolved.profile;
|
|
53
|
+
|
|
54
|
+
// Step 1 — Recompute
|
|
55
|
+
const hPrime = computeDigest(envelope.hash_function, observationBytes);
|
|
56
|
+
if (hPrime === null) return reject('hash_function_not_allowed');
|
|
57
|
+
|
|
58
|
+
// Step 2 — Compare (decoded bytes)
|
|
59
|
+
const envelopeDigest = hexToBytes(envelope.digest);
|
|
60
|
+
if (!bytesEq(hPrime, envelopeDigest)) return reject('digest_mismatch');
|
|
61
|
+
|
|
62
|
+
// Step 3 — Confirm inclusion (extraction rule under profile P)
|
|
63
|
+
|
|
64
|
+
// 3.1 The endpoint MUST serve the declared chain (decoded integers).
|
|
65
|
+
const endpointChainId = quantityToBigInt(await provider.eth_chainId());
|
|
66
|
+
if (endpointChainId !== decimalToBigInt(envelope.chain_id)) return reject('chain_id_mismatch');
|
|
67
|
+
|
|
68
|
+
// 3.2 Fetch the receipt; MUST exist; MUST have status == 1.
|
|
69
|
+
const receipt = await provider.eth_getTransactionReceipt(envelope.tx_hash);
|
|
70
|
+
if (!receipt) return reject('receipt_not_found');
|
|
71
|
+
if (quantityToBigInt(receipt.status) !== 1n) return reject('receipt_reverted');
|
|
72
|
+
|
|
73
|
+
// 3.3 Select the log by ARRAY POSITION within receipt.logs (zero-indexed).
|
|
74
|
+
// NOT the JSON-RPC logIndex field, which is block-scoped.
|
|
75
|
+
const position = Number(decimalToBigInt(envelope.receipt_log_position));
|
|
76
|
+
const log = receipt.logs[position];
|
|
77
|
+
if (log === undefined) return reject('log_position_out_of_range');
|
|
78
|
+
|
|
79
|
+
// 3.4 The selected log's address MUST match `contract` (20-byte compare).
|
|
80
|
+
if (!bytesEq(addressBytes(log.address), addressBytes(envelope.contract)))
|
|
81
|
+
return reject('contract_mismatch');
|
|
82
|
+
|
|
83
|
+
// 3.5 topics[0] MUST equal the profile's topic-0, and the log MUST contain
|
|
84
|
+
// exactly the profile's topic count. A log with the right topic-0 but the
|
|
85
|
+
// wrong count MUST be rejected. The data field is never used.
|
|
86
|
+
if (!Array.isArray(log.topics) || log.topics.length === 0) return reject('topic_count_invalid');
|
|
87
|
+
if (!bytesEq(hexToBytes(log.topics[0].toLowerCase()), hexToBytes(P.topic0)))
|
|
88
|
+
return reject('topic0_mismatch');
|
|
89
|
+
if (log.topics.length !== P.topicCount) return reject('topic_count_invalid');
|
|
90
|
+
|
|
91
|
+
// 3.6 The digest topic contains the digest verbatim (indexed fixed-size type).
|
|
92
|
+
const onChainDigest = hexToBytes(log.topics[P.digestTopicIndex].toLowerCase());
|
|
93
|
+
if (!bytesEq(onChainDigest, envelopeDigest)) return reject('onchain_digest_mismatch');
|
|
94
|
+
|
|
95
|
+
// 3.7 Profile-specific assertions.
|
|
96
|
+
const ctx = { onChainDigest, log, envelope };
|
|
97
|
+
for (const assertion of P.assertions) {
|
|
98
|
+
if (!assertion.check(ctx)) return reject(assertion.reason);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Step 4 — Confirm committer (address right-aligned in the 32-byte topic).
|
|
102
|
+
const onChainCommitter = hexToBytes(log.topics[P.committerTopicIndex].toLowerCase()).slice(12);
|
|
103
|
+
if (!bytesEq(onChainCommitter, addressBytes(envelope.committer))) return reject('committer_mismatch');
|
|
104
|
+
|
|
105
|
+
// Step 5 — Confirm block (decoded integers).
|
|
106
|
+
if (quantityToBigInt(receipt.blockNumber) !== decimalToBigInt(envelope.block_number))
|
|
107
|
+
return reject('block_number_mismatch');
|
|
108
|
+
|
|
109
|
+
// block_hash — MUST-when-present, against the receipt's own blockHash.
|
|
110
|
+
if (envelope.block_hash !== undefined) {
|
|
111
|
+
if (!bytesEq(hexToBytes(receipt.blockHash.toLowerCase()), hexToBytes(envelope.block_hash)))
|
|
112
|
+
return reject('block_hash_mismatch');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { valid: true };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Minimal JSON-RPC HTTP provider (uses global fetch, Node 18+).
|
|
120
|
+
* For stronger guarantees, supply your own provider that cross-checks
|
|
121
|
+
* multiple endpoints or verifies receipts against light-client headers.
|
|
122
|
+
*/
|
|
123
|
+
function httpProvider(rpcUrl) {
|
|
124
|
+
let id = 0;
|
|
125
|
+
const call = async (method, params) => {
|
|
126
|
+
const res = await fetch(rpcUrl, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: { 'content-type': 'application/json' },
|
|
129
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: ++id, method, params }),
|
|
130
|
+
});
|
|
131
|
+
const body = await res.json();
|
|
132
|
+
if (body.error) throw new Error(`${method}: ${body.error.message}`);
|
|
133
|
+
return body.result;
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
eth_chainId: () => call('eth_chainId', []),
|
|
137
|
+
eth_getTransactionReceipt: (txHash) => call('eth_getTransactionReceipt', [txHash]),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { verify, httpProvider };
|
package/LICENSE
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
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/reference-cli/README.md
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
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.
|