@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.
Files changed (2) hide show
  1. package/index.mjs +64 -28
  2. 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 a verifier that carries a Bitcoin node is not portable.
6
- // resolveByDiscovery / verifyAnchor take `substrateVerify` as an OPTIONAL injection: without it, an
7
- // anchor is honestly `unproven` (→ "HIGH pending"); WITH this package the anchor is cross-checked against
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
- // substrateVerify(anchor, root) { final, time } | null — the exact shape verifyAnchor expects:
11
- // · final:true → the root is committed in a Bitcoin block (≥ the lib's confirmation view)
12
- // · final:false the .ots is still calendar-pending (no Bitcoin attestation yet)
13
- // · null → the .ots does not attest THIS root (wrong digest) treated as not-verified
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
- // the lib ships a broken package.json main; createRequire sidesteps bundlers (proven in noosphere-anchor).
17
+ import { createHash } from 'node:crypto';
16
18
  const OTS = createRequire(import.meta.url)('opentimestamps');
17
19
 
18
- const hexToBytes = (hex) => { const h = hex.replace(/^sha256:/, ''); const o = new Uint8Array(h.length / 2); for (let i = 0; i < o.length; i++) o[i] = parseInt(h.substr(i * 2, 2), 16); return o; };
19
- const bytesEq = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
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
- // Extract the Bitcoin block height (if any) from a complete timestamp's attestations.
22
- function bitcoinHeight(det) {
23
- try {
24
- for (const [, atts] of det.timestamp.allAttestations()) {
25
- for (const a of (Array.isArray(atts) ? atts : [atts])) {
26
- if (a && (a._type === 'BitcoinBlockHeaderAttestation' || a.constructor?.name?.includes('Bitcoin')) && typeof a.height === 'number') return a.height;
27
- }
28
- }
29
- } catch { /* shape drift fall through */ }
30
- return null;
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
- const h = bitcoinHeight(det);
51
- return { final: true, time: h ? 'bitcoin-block-' + h : 'anchored' };
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.3",
4
- "description": "Opt-in Bitcoin (OpenTimestamps) substrateVerify for UST anchors — the cross-check ust-protocol takes as an injection, so the zero-dep verifier never embeds a blockchain. #68 witness / TOP time.",
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": ["index.mjs", "LICENSE", "README.md"],
9
- "keywords": ["ust", "opentimestamps", "bitcoin", "anchor", "substrate-verify", "witness"],
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": { "node": ">=20" },
12
- "dependencies": { "opentimestamps": "^0.4.9" },
13
- "repository": { "type": "git", "url": "git+https://github.com/thelabmd/UST-Protocol.git", "directory": "packages/ust-ots-verify" },
14
- "homepage": "https://github.com/thelabmd/UST-Protocol/tree/main/packages/ust-ots-verify#readme"
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
  }