@ust-protocol/ots-verify 1.0.0-rc.4 → 1.0.0-rc.6

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 +21 -6
  2. package/package.json +2 -2
package/index.mjs CHANGED
@@ -50,7 +50,7 @@ export function parseOtsBitcoin(ots) {
50
50
  return found ? { height: found.height, merkle: found.merkle, digest } : null;
51
51
  }
52
52
 
53
- export function makeSubstrateVerify({ upgrade = true, fetchImpl = fetch, explorers = EXPLORERS, minConfirmations = 6 } = {}) {
53
+ export function makeSubstrateVerify({ upgrade = true, fetchImpl = fetch, explorers = EXPLORERS, minConfirmations = 6, quorum = 2 } = {}) {
54
54
  return async function substrateVerify(anchor, root) {
55
55
  const sub = anchor?.substrate ?? anchor?.anchor?.substrate;
56
56
  if (sub && sub !== 'bitcoin-ots') return null; // not ours → router delegates onward
@@ -72,19 +72,34 @@ export function makeSubstrateVerify({ upgrade = true, fetchImpl = fetch, explore
72
72
  if (!parsed || typeof parsed.height !== 'number') return { final: false, time: 'unproven' };
73
73
  const wantMerkle = Buffer.from(parsed.merkle).reverse().toString('hex'); // block header displays reversed
74
74
 
75
+ // #71 — TRUST TERMINATION HONESTY. A SINGLE explorer is a TRUSTED ORACLE (it could serve a self-consistent
76
+ // fake block/merkle/tip). So finality REQUIRES AGREEMENT across ≥ `quorum` INDEPENDENT explorers, and the
77
+ // result is labelled `explorer-corroborated` — NOT trustless Bitcoin finality. Trustless needs a real node /
78
+ // SPV header-chain (PoW-validated); that is an OPERATOR plugin injected through this SAME substrateVerify
79
+ // seam. This plugin is honest about its ceiling; a reachable explorer that DISAGREES on the merkle root is a
80
+ // definitive NO.
81
+ const need = Math.max(1, Math.min(quorum, explorers.length));
82
+ // #71-followup P1 — query ALL configured explorers BEFORE deciding: a disagreement by a LATER source must
83
+ // still count (early-returning on quorum could miss it). ANY reachable source that disagrees on the merkle
84
+ // root is a DEFINITIVE NO, even if others agree. And a quorum of ONE is NOT `corroborated` — it is a single
85
+ // trusted oracle, honestly labelled `explorer-single`.
86
+ let agree = 0, time = null, conflict = false;
75
87
  for (const base of explorers) {
76
88
  try {
77
89
  const hash = (await (await fetchImpl(`${base}/block-height/${parsed.height}`, { signal: AbortSignal.timeout(10000) })).text()).trim();
78
90
  if (!/^[0-9a-f]{64}$/.test(hash)) continue;
79
91
  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
92
+ if (!blk || typeof blk.merkle_root !== 'string') continue;
93
+ if (blk.merkle_root !== wantMerkle) { conflict = true; continue; } // an independent source DISAGREES — keep querying, but this is a NO
81
94
  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 };
95
+ if (!Number.isFinite(tip) || tip - parsed.height + 1 < minConfirmations) continue; // this source lags on burial → don't count
96
+ agree++;
97
+ time = time || (blk.timestamp ? new Date(blk.timestamp * 1000).toISOString().slice(0, 19) + 'Z' : 'bitcoin-block-' + parsed.height);
85
98
  } catch { /* explorer unreachable — try the next */ }
86
99
  }
87
- return { final: false, time: 'unproven' }; // no explorer could confirm honest unproven
100
+ if (conflict) return { final: false, time: 'unproven' }; // ANY reachable disagreementdefinitive NO
101
+ if (agree < need) return { final: false, time: 'unproven', detail: `only ${agree}/${need} independent explorers corroborated` };
102
+ return { final: true, time, assurance: need >= 2 ? 'explorer-corroborated' : 'explorer-single', explorers: agree };
88
103
  };
89
104
  }
90
105
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ust-protocol/ots-verify",
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.",
3
+ "version": "1.0.0-rc.6",
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 / #71 explorer-corroborated (2-of-N).",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
7
7
  "exports": "./index.mjs",