@ust-protocol/ots-verify 1.0.0-rc.3 → 1.0.0-rc.4
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/index.mjs +64 -28
- package/package.json +30 -8
package/index.mjs
CHANGED
|
@@ -1,41 +1,59 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// @ust-protocol/ots-verify — the OPT-IN Bitcoin substrateVerify for UST anchors (#68 Ф1b).
|
|
2
|
+
// @ust-protocol/ots-verify — the OPT-IN Bitcoin substrateVerify for UST anchors (#68 Ф1b / #69 A2).
|
|
3
3
|
//
|
|
4
4
|
// WHY A SEPARATE PACKAGE: the zero-dependency reference verifier (ust-protocol) must never embed a
|
|
5
|
-
// blockchain / the heavy opentimestamps lib
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// Bitcoin. An integrator that wants the cross-check installs this; nobody is forced to.
|
|
5
|
+
// blockchain / the heavy opentimestamps lib. resolveByDiscovery / verifyAnchor take `substrateVerify` as an
|
|
6
|
+
// OPTIONAL injection: without it, an anchor is honestly `unproven`; WITH this package it is cross-checked
|
|
7
|
+
// against Bitcoin.
|
|
9
8
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
9
|
+
// #69 A2 (P0) — `isTimestampComplete()` only means "a Bitcoin attestation EXISTS in the .ots tree"; it does
|
|
10
|
+
// NOT mean that block actually commits the root, nor that it is buried. A fabricated 'complete' .ots would
|
|
11
|
+
// pass. So finality now REQUIRES: (1) the .ots attests THIS root; (2) the committed value equals the REAL
|
|
12
|
+
// merkle root of the Bitcoin block at the attested height (fetched from a read-only explorer — public
|
|
13
|
+
// consensus, the explorer only mirrors it); (3) the block is buried under >= minConfirmations (default 6,
|
|
14
|
+
// §17). The explorer is untrusted: a wrong answer fails the merkle match (claim ≠ proof); unreachable →
|
|
15
|
+
// `unproven`, never a false `final`.
|
|
14
16
|
import { createRequire } from 'node:module';
|
|
15
|
-
|
|
17
|
+
import { createHash } from 'node:crypto';
|
|
16
18
|
const OTS = createRequire(import.meta.url)('opentimestamps');
|
|
17
19
|
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
+
const EXPLORERS = ['https://blockstream.info/api', 'https://mempool.space/api'];
|
|
21
|
+
const OTS_BTC_TAG = Buffer.from([0x05, 0x88, 0x96, 0x0d, 0x73, 0xd7, 0x19, 0x01]);
|
|
22
|
+
const sha256 = (b) => createHash('sha256').update(b).digest();
|
|
23
|
+
const hexToBytes = (hex) => Buffer.from(hex.replace(/^sha256:/, ''), 'hex');
|
|
24
|
+
const bytesEq = (a, b) => Buffer.from(a).equals(Buffer.from(b));
|
|
20
25
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
// Parse an OpenTimestamps proof (canonical Timestamp.deserialize grammar: 0xff separates sibling branches at
|
|
27
|
+
// a node, an op recurses into a sub-timestamp on the transformed message, an attestation fixes the node's
|
|
28
|
+
// message) down to its BitcoinBlockHeaderAttestation → { height, merkle (internal byte order) }. sha256/
|
|
29
|
+
// append/prepend only — an unsupported op throws and the parse fails closed. (Same logic proven in the web
|
|
30
|
+
// verifier docs/ust-resolve.mjs, against a live block header.)
|
|
31
|
+
export function parseOtsBitcoin(ots) {
|
|
32
|
+
let pos = 31; pos++; /* major version */ pos++; /* file-hash op */
|
|
33
|
+
const digest = ots.subarray(pos, pos + 32); pos += 32;
|
|
34
|
+
const readVarint = () => { let r = 0, sh = 0; for (;;) { const b = ots[pos++]; r += (b & 0x7f) * (2 ** sh); if (!(b & 0x80)) break; sh += 7; } return r; };
|
|
35
|
+
let found = null;
|
|
36
|
+
const applyOp = (tag, msg) => {
|
|
37
|
+
if (tag === 0xf0) { const n = readVarint(); const a = ots.subarray(pos, pos + n); pos += n; return Buffer.concat([msg, a]); }
|
|
38
|
+
if (tag === 0xf1) { const n = readVarint(); const a = ots.subarray(pos, pos + n); pos += n; return Buffer.concat([a, msg]); }
|
|
39
|
+
if (tag === 0x08) return sha256(msg);
|
|
40
|
+
throw new Error('ots op 0x' + tag.toString(16) + ' unsupported');
|
|
41
|
+
};
|
|
42
|
+
const doOne = (tag, msg) => {
|
|
43
|
+
if (tag === 0x00) {
|
|
44
|
+
const at = ots.subarray(pos, pos + 8); pos += 8; const len = readVarint(); const payload = ots.subarray(pos, pos + len); pos += len;
|
|
45
|
+
if (at.equals(OTS_BTC_TAG)) { let h = 0, sh = 0, p = 0; for (;;) { const b = payload[p++]; h += (b & 0x7f) * (2 ** sh); if (!(b & 0x80)) break; sh += 7; } found = { height: h, merkle: Buffer.from(msg) }; }
|
|
46
|
+
} else { deserialize(applyOp(tag, msg)); }
|
|
47
|
+
};
|
|
48
|
+
function deserialize(msg) { let tag = ots[pos++]; while (tag === 0xff) { doOne(ots[pos++], msg); tag = ots[pos++]; } doOne(tag, msg); }
|
|
49
|
+
deserialize(digest);
|
|
50
|
+
return found ? { height: found.height, merkle: found.merkle, digest } : null;
|
|
31
51
|
}
|
|
32
52
|
|
|
33
|
-
export function makeSubstrateVerify({ upgrade = true } = {}) {
|
|
53
|
+
export function makeSubstrateVerify({ upgrade = true, fetchImpl = fetch, explorers = EXPLORERS, minConfirmations = 6 } = {}) {
|
|
34
54
|
return async function substrateVerify(anchor, root) {
|
|
35
|
-
// handle ONLY bitcoin-ots — return null for any other substrate so a multi-substrate router
|
|
36
|
-
// (ust-protocol combineSubstrates) can delegate to the next plugin.
|
|
37
55
|
const sub = anchor?.substrate ?? anchor?.anchor?.substrate;
|
|
38
|
-
if (sub && sub !== 'bitcoin-ots') return null;
|
|
56
|
+
if (sub && sub !== 'bitcoin-ots') return null; // not ours → router delegates onward
|
|
39
57
|
const otsB64 = anchor?.ots ?? anchor?.anchor?.ots;
|
|
40
58
|
if (!otsB64 || typeof root !== 'string') return null;
|
|
41
59
|
let det;
|
|
@@ -47,8 +65,26 @@ export function makeSubstrateVerify({ upgrade = true } = {}) {
|
|
|
47
65
|
try { await OTS.upgrade(det); } catch { /* calendar unreachable → stays pending */ }
|
|
48
66
|
}
|
|
49
67
|
if (!det.timestamp.isTimestampComplete()) return { final: false, time: 'unproven' };
|
|
50
|
-
|
|
51
|
-
|
|
68
|
+
|
|
69
|
+
// #69 A2 — parse to the Bitcoin attestation and PROVE it against the real chain (not just structure).
|
|
70
|
+
let parsed;
|
|
71
|
+
try { parsed = parseOtsBitcoin(Buffer.from(det.serializeToBytes())); } catch { return { final: false, time: 'unproven' }; }
|
|
72
|
+
if (!parsed || typeof parsed.height !== 'number') return { final: false, time: 'unproven' };
|
|
73
|
+
const wantMerkle = Buffer.from(parsed.merkle).reverse().toString('hex'); // block header displays reversed
|
|
74
|
+
|
|
75
|
+
for (const base of explorers) {
|
|
76
|
+
try {
|
|
77
|
+
const hash = (await (await fetchImpl(`${base}/block-height/${parsed.height}`, { signal: AbortSignal.timeout(10000) })).text()).trim();
|
|
78
|
+
if (!/^[0-9a-f]{64}$/.test(hash)) continue;
|
|
79
|
+
const blk = await (await fetchImpl(`${base}/block/${hash}`, { signal: AbortSignal.timeout(10000) })).json();
|
|
80
|
+
if (!blk || blk.merkle_root !== wantMerkle) return { final: false, time: 'unproven' }; // definitive NO
|
|
81
|
+
const tip = Number((await (await fetchImpl(`${base}/blocks/tip/height`, { signal: AbortSignal.timeout(10000) })).text()).trim());
|
|
82
|
+
const confirmations = Number.isFinite(tip) ? tip - parsed.height + 1 : 0;
|
|
83
|
+
if (confirmations < minConfirmations) return { final: false, time: 'unproven' }; // not yet buried
|
|
84
|
+
return { final: true, time: blk.timestamp ? new Date(blk.timestamp * 1000).toISOString().slice(0, 19) + 'Z' : 'bitcoin-block-' + parsed.height };
|
|
85
|
+
} catch { /* explorer unreachable — try the next */ }
|
|
86
|
+
}
|
|
87
|
+
return { final: false, time: 'unproven' }; // no explorer could confirm → honest unproven
|
|
52
88
|
};
|
|
53
89
|
}
|
|
54
90
|
|
package/package.json
CHANGED
|
@@ -1,15 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ust-protocol/ots-verify",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
4
|
-
"description": "Opt-in Bitcoin
|
|
3
|
+
"version": "1.0.0-rc.4",
|
|
4
|
+
"description": "Opt-in Bitcoin/OpenTimestamps substrateVerify for UST anchors — cross-checks a genesis/hour root against a REAL Bitcoin block header + >=6 confirmations. #68 witness / #69 A2.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
7
7
|
"exports": "./index.mjs",
|
|
8
|
-
"files": [
|
|
9
|
-
|
|
8
|
+
"files": [
|
|
9
|
+
"index.mjs",
|
|
10
|
+
"LICENSE",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ust",
|
|
15
|
+
"opentimestamps",
|
|
16
|
+
"bitcoin",
|
|
17
|
+
"anchor",
|
|
18
|
+
"substrate-verify",
|
|
19
|
+
"witness"
|
|
20
|
+
],
|
|
10
21
|
"license": "Apache-2.0",
|
|
11
|
-
"engines": {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"opentimestamps": "^0.4.9"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/thelabmd/UST-Protocol.git",
|
|
31
|
+
"directory": "packages/ust-ots-verify"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/thelabmd/UST-Protocol/tree/main/packages/ust-ots-verify#readme",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "node --test"
|
|
36
|
+
}
|
|
15
37
|
}
|