G56-04: multi-explorer cross-check (fail on disagreement) + trustMode field in verifyOtsProof result (multi-explorer-checked/single-explorer-checked/structural-only) + UI trust-mode labels + remove misleading lite-client comments
This commit is contained in:
@@ -9,10 +9,10 @@
|
||||
| Severity | Total | Fixed | In Progress | Not Started |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Critical | 2 | 2 | 0 | 0 |
|
||||
| High | 6 | 3 | 0 | 3 |
|
||||
| High | 6 | 4 | 0 | 2 |
|
||||
| Medium | 8 | 2 | 0 | 6 |
|
||||
| Low / Info | 4 | 0 | 0 | 4 |
|
||||
| **Total** | **20** | **7** | **0** | **13** |
|
||||
| **Total** | **20** | **8** | **0** | **12** |
|
||||
|
||||
## Findings status
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
| G56-01 | Critical | Verifier accepts no PQ signatures and missing signatures as successful | ✅ Fixed | v0.0.12 | Strict mandatory algorithm policy, fail-closed on malformed input, structured result fields, 11 adversarial tests |
|
||||
| G56-02 | Critical | Earliest-valid-anchor rule not implemented; relay discovery selects latest replaceable event | ✅ Fixed | v0.0.14–v0.0.16 | Switched to non-replaceable kind 9999, append-only upgrades with `upgrade_of` tag, `selectCanonicalProofCarrier()` function, removed `limit: 1` from relay queries, updated all docs |
|
||||
| G56-03 | High | Outer wrapper identity not bound to embedded identity | ✅ Fixed | v0.0.14 | Added `outerPubkey` and `expectedAuthor` options to `verifyNIPQRContent()`, `identityBound` field in result, `validForMigration` requires identity binding, 4 identity binding tests |
|
||||
| G56-04 | High | Bitcoin verification trusts public API JSON, doesn't validate headers/PoW | ❌ Not started | — | Needs trust-mode labeling or real header validation |
|
||||
| G56-04 | High | Bitcoin verification trusts public API JSON, doesn't validate headers/PoW | ✅ Fixed | v0.0.21 | Tier 1+2: rewrote `fetchBitcoinBlockHeader()` to query ALL providers and cross-check merkle roots (fail on disagreement); added `trustMode` field to `verifyOtsProof()` result (`multi-explorer-checked` / `single-explorer-checked` / `structural-only`); updated verify.html + index.html UI to display trust mode label; removed misleading "lite-client verification" / "independently verifiable" comments. Full light-client PoW verification (Tier 3) remains as future work. 2 test assertions added (trustMode present on verified proof, structural-only on pending) |
|
||||
| G56-05 | High | Signer output reconstructed without validating returned ID/sig/pubkey/fields | ✅ Fixed | v0.0.20 | Added `validateSignerOutput()` helper: checks all required fields present + typed, verifies no template field (content/tags/created_at/kind) changed, verifies pubkey equals expected identity, recomputes + verifies event ID, verifies Schnorr signature, rejects extra fields. Replaced reconstruction at all 4 `signEvent()` call sites (2 in index.html, 2 in verify.html) with validation + retain-exact-signed-object. 15 new tests (mutated content/tags/kind/created_at, wrong pubkey, wrong ID, invalid sig, extra field, missing field, non-object, malformed id/sig, tag element changed) |
|
||||
| G56-06 | High | New seed continuity asserted, not proven — no successor signature, no identity match | ❌ Not started | — | Protocol design decision needed |
|
||||
| G56-07 | High | Verifier throws on malformed relay/paste input | ✅ Fixed | v0.0.12, v0.0.19 | v0.0.12 wrapped base64/hex in try/catch + verifyNIPQRContent fail closed + verify.html call-site wrapping. v0.0.19 added structural validation + try/catch to `verifyNostrEvent()` (returns false on malformed input), wrapped `parseOtsFile()` body in try/catch (returns null), added `MAX_OTS_PROOF_SIZE` (1 MiB) and `MAX_EVENT_JSON_SIZE` (64 KiB) size guards, 23 fuzz/property tests (random JSON, random bytes, truncation, oversized inputs, wrong-typed objects) |
|
||||
@@ -115,6 +115,14 @@
|
||||
- `verify.html`: upgrade (now validated)
|
||||
- 15 new tests: valid output, mutated content/tags/kind/created_at, wrong pubkey, wrong ID, invalid sig, extra field, missing field, non-object, malformed id/sig, tag element changed, no-expectedPubkey backward compat
|
||||
|
||||
### v0.0.21 — G56-04 fix (Bitcoin trust-mode labeling + multi-explorer cross-check)
|
||||
- Rewrote `fetchBitcoinBlockHeader()` to query ALL providers (blockstream + mempool) and cross-check their merkle roots; fails on disagreement (turns fallback-redundancy into a cross-check)
|
||||
- Added `trustMode` field to `verifyOtsProof()` return value: `multi-explorer-checked` (both providers agreed), `single-explorer-checked` (only one responded), `structural-only` (no Bitcoin attestation verified)
|
||||
- Updated verify.html (2 locations) and index.html (1 location) to display the trust mode label alongside verification results
|
||||
- Removed misleading "lite-client verification" / "independently verifiable against the Bitcoin PoW chain" comments from code
|
||||
- Full light-client PoW/difficulty/chain-linkage verification (Tier 3) remains as future work
|
||||
- 2 new test assertions: trustMode present + explorer-checked on verified proof, structural-only on pending proof
|
||||
|
||||
## Test count
|
||||
|
||||
- **Before remediation:** 38 tests
|
||||
@@ -124,4 +132,5 @@
|
||||
- **After G56-16:** 65 tests (docs-only)
|
||||
- **After G56-07:** 88 tests
|
||||
- **After G56-05:** 103 tests
|
||||
- **After G56-04:** 103 tests (2 assertions added to existing tests)
|
||||
- **All passing:** ✅
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "post_quantum_nostr",
|
||||
"version": "0.0.20",
|
||||
"version": "0.0.21",
|
||||
"description": "A migration strategy for bringing post-quantum security to Nostr without breaking the social graph, without requiring consensus on a single post-quantum algorithm, and without forcing existing users to abandon their identities.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -311,6 +311,10 @@ describe('OTS verifier (verifyOtsProof) — F-C3 binding', () => {
|
||||
assert.equal(result.verified, true, 'should verify against Bitcoin block 358391');
|
||||
assert.equal(result.bitcoinAttestations.length, 1);
|
||||
assert.equal(result.bitcoinAttestations[0].height, 358391);
|
||||
// G56-04: trustMode must be present and indicate explorer-checked trust
|
||||
assert.ok(result.trustMode, 'trustMode field must be present');
|
||||
assert.ok(['multi-explorer-checked', 'single-explorer-checked'].includes(result.trustMode),
|
||||
`trustMode should be explorer-checked, got: ${result.trustMode}`);
|
||||
});
|
||||
|
||||
test('F-C3: wrong expected digest fails with "Target digest mismatch"', async () => {
|
||||
@@ -334,6 +338,8 @@ describe('OTS verifier (verifyOtsProof) — F-C3 binding', () => {
|
||||
const result = await m.verifyOtsProof(ots);
|
||||
assert.equal(result.verified, false);
|
||||
assert.ok(result.errors.some(e => e.includes('no Bitcoin attestations') || e.includes('pending')), 'should report no Bitcoin attestations');
|
||||
// G56-04: trustMode should be 'structural-only' when no Bitcoin attestation is verified
|
||||
assert.equal(result.trustMode, 'structural-only', 'pending proof should have structural-only trust mode');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+7
-2
@@ -1703,9 +1703,14 @@
|
||||
if (verifyResult.verified) {
|
||||
const att = verifyResult.bitcoinAttestations[0];
|
||||
const date = att ? new Date(att.time * 1000).toISOString().substring(0, 19) : 'unknown';
|
||||
otsLog(`Poll ${pollCount}: Bitcoin attestation VERIFIED (block ${att ? att.height : '?'}, mined ${date} UTC). Proof: ${pendingOtsBytes.length} bytes.`);
|
||||
const trustLabel = verifyResult.trustMode === 'multi-explorer-checked'
|
||||
? 'multi-explorer-checked'
|
||||
: verifyResult.trustMode === 'single-explorer-checked'
|
||||
? 'single-explorer-checked'
|
||||
: verifyResult.trustMode || 'unknown';
|
||||
otsLog(`Poll ${pollCount}: Bitcoin attestation VERIFIED (block ${att ? att.height : '?'}, mined ${date} UTC, trust: ${trustLabel}). Proof: ${pendingOtsBytes.length} bytes.`);
|
||||
setKeyIcon('pqOtsConfirm', 'Done');
|
||||
confirmDetail.textContent = `Verified on Bitcoin blockchain (block ${att ? att.height : '?'})!`;
|
||||
confirmDetail.textContent = `Verified on Bitcoin (block ${att ? att.height : '?'}, ${trustLabel})`;
|
||||
if (otsPollInterval) { clearInterval(otsPollInterval); otsPollInterval = null; }
|
||||
|
||||
const saved = loadPendingOts();
|
||||
|
||||
+81
-21
@@ -1836,8 +1836,13 @@ function _classifyAttestation(tag, payload, digest, attestations) {
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Bitcoin API endpoints for fetching block headers (for lite-client verification).
|
||||
* We try multiple providers for resilience.
|
||||
* Bitcoin block explorer API providers.
|
||||
*
|
||||
* G56-04: These are trusted public APIs, NOT a Bitcoin light-client. The block
|
||||
* headers they return are NOT independently verified against PoW, difficulty, or
|
||||
* chain linkage. We cross-check both providers and fail on disagreement to make
|
||||
* coordinated false responses harder, but this is still "multi-explorer-checked"
|
||||
* trust, not "header-chain-verified" or trustless.
|
||||
*/
|
||||
const BITCOIN_API_PROVIDERS = [
|
||||
{ name: 'blockstream', blockHash: (h) => `https://blockstream.info/api/block-height/${h}`, block: (hash) => `https://blockstream.info/api/block/${hash}` },
|
||||
@@ -1845,15 +1850,23 @@ const BITCOIN_API_PROVIDERS = [
|
||||
];
|
||||
|
||||
/**
|
||||
* Fetch a Bitcoin block header (merkle root + timestamp) from a public API.
|
||||
* Uses lite-client verification: we trust the API for the block header, which
|
||||
* is the standard OTS lite-client model (the block header is independently
|
||||
* verifiable against the Bitcoin PoW chain).
|
||||
* Fetch a Bitcoin block header (merkle root + timestamp) from public explorer APIs.
|
||||
*
|
||||
* G56-04: This is NOT lite-client verification. We trust the APIs for the block
|
||||
* header data. To make coordinated false responses harder, we query ALL providers
|
||||
* and require them to agree on the merkle root. If they disagree, we fail.
|
||||
*
|
||||
* Trust mode: 'multi-explorer-checked' if all queried providers agree.
|
||||
* This is stronger than single-provider trust but is NOT trustless — a
|
||||
* compromise of both providers could still falsify a result.
|
||||
*
|
||||
* @param {number} height - Bitcoin block height
|
||||
* @returns {Promise<{merkleroot: string, time: number, height: number}>}
|
||||
* @returns {Promise<{merkleroot: string, time: number, height: number, trustMode: string, providers: string[]}>}
|
||||
* @throws {Error} if providers disagree or all fail
|
||||
*/
|
||||
async function fetchBitcoinBlockHeader(height) {
|
||||
const results = [];
|
||||
|
||||
for (const provider of BITCOIN_API_PROVIDERS) {
|
||||
try {
|
||||
// 1. Get block hash from height
|
||||
@@ -1869,37 +1882,74 @@ async function fetchBitcoinBlockHeader(height) {
|
||||
if (!blockData.merkle_root && !blockData.merkleroot) continue;
|
||||
if (!blockData.timestamp && !blockData.time) continue;
|
||||
|
||||
return {
|
||||
merkleroot: blockData.merkle_root || blockData.merkleroot,
|
||||
results.push({
|
||||
provider: provider.name,
|
||||
merkleroot: (blockData.merkle_root || blockData.merkleroot).toLowerCase(),
|
||||
time: blockData.timestamp || blockData.time,
|
||||
height
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`[ots] Bitcoin API ${provider.name} failed for height ${height}:`, e.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not fetch Bitcoin block header for height ${height} from any provider`);
|
||||
|
||||
if (results.length === 0) {
|
||||
throw new Error(`Could not fetch Bitcoin block header for height ${height} from any provider`);
|
||||
}
|
||||
|
||||
// G56-04: Cross-check — if multiple providers responded, they must agree
|
||||
if (results.length > 1) {
|
||||
const firstRoot = results[0].merkleroot;
|
||||
for (let i = 1; i < results.length; i++) {
|
||||
if (results[i].merkleroot !== firstRoot) {
|
||||
const providerList = results.map(r => `${r.provider}:${r.merkleroot.substring(0, 16)}...`).join(', ');
|
||||
throw new Error(`Bitcoin providers disagree on merkle root for block ${height}: ${providerList}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine trust mode based on how many providers agreed
|
||||
const trustMode = results.length >= 2
|
||||
? 'multi-explorer-checked'
|
||||
: 'single-explorer-checked';
|
||||
|
||||
return {
|
||||
merkleroot: results[0].merkleroot,
|
||||
time: results[0].time,
|
||||
height,
|
||||
trustMode,
|
||||
providers: results.map(r => r.provider)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Cryptographically verify an OTS proof against an expected target digest.
|
||||
* Verify an OTS proof against an expected target digest.
|
||||
*
|
||||
* This performs REAL verification:
|
||||
* G56-04: This performs Merkle-path verification against block headers obtained
|
||||
* from public explorer APIs. The block headers are NOT independently verified
|
||||
* against Bitcoin PoW/difficulty/chain-linkage — they are trusted from the APIs.
|
||||
* The `trustMode` field in the result indicates the trust level:
|
||||
* - 'multi-explorer-checked': multiple providers agreed on the merkle root
|
||||
* - 'single-explorer-checked': only one provider responded
|
||||
* - 'structural-only': no Bitcoin attestation was verified (pending/none)
|
||||
*
|
||||
* Steps:
|
||||
* 1. Parses the OTS file (op stream, not byte-pattern search)
|
||||
* 2. Checks the proof's target digest equals the expected digest (F-C3 fix)
|
||||
* 3. Walks the op tree to compute the commitment at each attestation
|
||||
* 4. For Bitcoin attestations: fetches the block header and checks the
|
||||
* computed merkle root matches the block's merkle root
|
||||
* 4. For Bitcoin attestations: fetches the block header (cross-checked across
|
||||
* providers) and checks the computed merkle root matches
|
||||
*
|
||||
* @param {Uint8Array} otsBytes - The .ots file bytes
|
||||
* @param {string} [expectedDigestHex] - The expected target digest (hex). If
|
||||
* provided, the proof's target digest must match this exactly (F-C3 binding).
|
||||
* @returns {Promise<{verified: boolean, targetDigest: string, attestations: Array, bitcoinAttestations: Array, errors: Array}>}
|
||||
* - verified: true if at least one Bitcoin attestation is cryptographically valid
|
||||
* @returns {Promise<{verified: boolean, targetDigest: string, attestations: Array, bitcoinAttestations: Array, trustMode: string, errors: Array}>}
|
||||
* - verified: true if at least one Bitcoin attestation's merkle root matches
|
||||
* - targetDigest: hex of the proof's target digest
|
||||
* - attestations: all attestations found (parsed)
|
||||
* - bitcoinAttestations: verified Bitcoin attestations with block time + height
|
||||
* - trustMode: 'multi-explorer-checked' | 'single-explorer-checked' | 'structural-only'
|
||||
* - errors: list of error messages (e.g., digest mismatch, pending only)
|
||||
*/
|
||||
export async function verifyOtsProof(otsBytes, expectedDigestHex) {
|
||||
@@ -1910,10 +1960,10 @@ export async function verifyOtsProof(otsBytes, expectedDigestHex) {
|
||||
try {
|
||||
parsed = parseOtsFile(otsBytes);
|
||||
} catch (e) {
|
||||
return { verified: false, targetDigest: null, attestations: [], bitcoinAttestations: [], errors: [`Failed to parse OTS file: ${e.message}`] };
|
||||
return { verified: false, targetDigest: null, attestations: [], bitcoinAttestations: [], trustMode: 'structural-only', errors: [`Failed to parse OTS file: ${e.message}`] };
|
||||
}
|
||||
if (!parsed) {
|
||||
return { verified: false, targetDigest: null, attestations: [], bitcoinAttestations: [], errors: ['Invalid OTS file format'] };
|
||||
return { verified: false, targetDigest: null, attestations: [], bitcoinAttestations: [], trustMode: 'structural-only', errors: ['Invalid OTS file format'] };
|
||||
}
|
||||
|
||||
const targetDigestHex = bytesToHex(parsed.targetDigest);
|
||||
@@ -1924,7 +1974,7 @@ export async function verifyOtsProof(otsBytes, expectedDigestHex) {
|
||||
const actual = targetDigestHex.toLowerCase();
|
||||
if (expected !== actual) {
|
||||
errors.push(`Target digest mismatch: proof commits to ${actual.substring(0, 16)}... but expected ${expected.substring(0, 16)}...`);
|
||||
return { verified: false, targetDigest: targetDigestHex, attestations: parsed.attestations, bitcoinAttestations: [], errors };
|
||||
return { verified: false, targetDigest: targetDigestHex, attestations: parsed.attestations, bitcoinAttestations: [], trustMode: 'structural-only', errors };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1938,14 +1988,23 @@ export async function verifyOtsProof(otsBytes, expectedDigestHex) {
|
||||
} else {
|
||||
errors.push('Proof contains no Bitcoin attestations');
|
||||
}
|
||||
return { verified: false, targetDigest: targetDigestHex, attestations: parsed.attestations, bitcoinAttestations: [], errors };
|
||||
return { verified: false, targetDigest: targetDigestHex, attestations: parsed.attestations, bitcoinAttestations: [], trustMode: 'structural-only', errors };
|
||||
}
|
||||
|
||||
// 4. Verify each Bitcoin attestation against the actual Bitcoin block header
|
||||
// G56-04: block headers are cross-checked across providers; trustMode is
|
||||
// propagated from fetchBitcoinBlockHeader.
|
||||
const verified = [];
|
||||
let trustMode = 'structural-only';
|
||||
for (const att of bitcoinAttestations) {
|
||||
try {
|
||||
const blockHeader = await fetchBitcoinBlockHeader(att.height);
|
||||
// Track the strongest trust mode seen across all attestations
|
||||
if (blockHeader.trustMode === 'multi-explorer-checked') {
|
||||
trustMode = 'multi-explorer-checked';
|
||||
} else if (blockHeader.trustMode === 'single-explorer-checked' && trustMode === 'structural-only') {
|
||||
trustMode = 'single-explorer-checked';
|
||||
}
|
||||
// The attestation's digest is the merkle root (after walking the op tree).
|
||||
// Bitcoin OTS stores the digest in reversed byte order (little-endian).
|
||||
const computedMerkleRoot = bytesToHex(att.digest.slice().reverse());
|
||||
@@ -1968,6 +2027,7 @@ export async function verifyOtsProof(otsBytes, expectedDigestHex) {
|
||||
targetDigest: targetDigestHex,
|
||||
attestations: parsed.attestations,
|
||||
bitcoinAttestations: verified,
|
||||
trustMode,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"VERSION": "v0.0.20",
|
||||
"VERSION_NUMBER": "0.0.20",
|
||||
"BUILD_DATE": "2026-07-24T13:42:06.579Z"
|
||||
"VERSION": "v0.0.21",
|
||||
"VERSION_NUMBER": "0.0.21",
|
||||
"BUILD_DATE": "2026-07-24T14:15:01.028Z"
|
||||
}
|
||||
|
||||
+36
-8
@@ -10656,6 +10656,7 @@ var BITCOIN_API_PROVIDERS = [
|
||||
{ name: "mempool", blockHash: (h) => `https://mempool.space/api/block-height/${h}`, block: (hash) => `https://mempool.space/api/block/${hash}` }
|
||||
];
|
||||
async function fetchBitcoinBlockHeader(height) {
|
||||
const results = [];
|
||||
for (const provider of BITCOIN_API_PROVIDERS) {
|
||||
try {
|
||||
const hashResp = await fetch(provider.blockHash(height), { headers: { "Accept": "text/plain" } });
|
||||
@@ -10667,17 +10668,37 @@ async function fetchBitcoinBlockHeader(height) {
|
||||
const blockData = await blockResp.json();
|
||||
if (!blockData.merkle_root && !blockData.merkleroot) continue;
|
||||
if (!blockData.timestamp && !blockData.time) continue;
|
||||
return {
|
||||
merkleroot: blockData.merkle_root || blockData.merkleroot,
|
||||
results.push({
|
||||
provider: provider.name,
|
||||
merkleroot: (blockData.merkle_root || blockData.merkleroot).toLowerCase(),
|
||||
time: blockData.timestamp || blockData.time,
|
||||
height
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`[ots] Bitcoin API ${provider.name} failed for height ${height}:`, e.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not fetch Bitcoin block header for height ${height} from any provider`);
|
||||
if (results.length === 0) {
|
||||
throw new Error(`Could not fetch Bitcoin block header for height ${height} from any provider`);
|
||||
}
|
||||
if (results.length > 1) {
|
||||
const firstRoot = results[0].merkleroot;
|
||||
for (let i = 1; i < results.length; i++) {
|
||||
if (results[i].merkleroot !== firstRoot) {
|
||||
const providerList = results.map((r) => `${r.provider}:${r.merkleroot.substring(0, 16)}...`).join(", ");
|
||||
throw new Error(`Bitcoin providers disagree on merkle root for block ${height}: ${providerList}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const trustMode = results.length >= 2 ? "multi-explorer-checked" : "single-explorer-checked";
|
||||
return {
|
||||
merkleroot: results[0].merkleroot,
|
||||
time: results[0].time,
|
||||
height,
|
||||
trustMode,
|
||||
providers: results.map((r) => r.provider)
|
||||
};
|
||||
}
|
||||
async function verifyOtsProof(otsBytes, expectedDigestHex) {
|
||||
const errors = [];
|
||||
@@ -10685,10 +10706,10 @@ async function verifyOtsProof(otsBytes, expectedDigestHex) {
|
||||
try {
|
||||
parsed = parseOtsFile(otsBytes);
|
||||
} catch (e) {
|
||||
return { verified: false, targetDigest: null, attestations: [], bitcoinAttestations: [], errors: [`Failed to parse OTS file: ${e.message}`] };
|
||||
return { verified: false, targetDigest: null, attestations: [], bitcoinAttestations: [], trustMode: "structural-only", errors: [`Failed to parse OTS file: ${e.message}`] };
|
||||
}
|
||||
if (!parsed) {
|
||||
return { verified: false, targetDigest: null, attestations: [], bitcoinAttestations: [], errors: ["Invalid OTS file format"] };
|
||||
return { verified: false, targetDigest: null, attestations: [], bitcoinAttestations: [], trustMode: "structural-only", errors: ["Invalid OTS file format"] };
|
||||
}
|
||||
const targetDigestHex = bytesToHex3(parsed.targetDigest);
|
||||
if (expectedDigestHex) {
|
||||
@@ -10696,7 +10717,7 @@ async function verifyOtsProof(otsBytes, expectedDigestHex) {
|
||||
const actual = targetDigestHex.toLowerCase();
|
||||
if (expected !== actual) {
|
||||
errors.push(`Target digest mismatch: proof commits to ${actual.substring(0, 16)}... but expected ${expected.substring(0, 16)}...`);
|
||||
return { verified: false, targetDigest: targetDigestHex, attestations: parsed.attestations, bitcoinAttestations: [], errors };
|
||||
return { verified: false, targetDigest: targetDigestHex, attestations: parsed.attestations, bitcoinAttestations: [], trustMode: "structural-only", errors };
|
||||
}
|
||||
}
|
||||
const bitcoinAttestations = parsed.attestations.filter((a) => a.type === "bitcoin");
|
||||
@@ -10707,12 +10728,18 @@ async function verifyOtsProof(otsBytes, expectedDigestHex) {
|
||||
} else {
|
||||
errors.push("Proof contains no Bitcoin attestations");
|
||||
}
|
||||
return { verified: false, targetDigest: targetDigestHex, attestations: parsed.attestations, bitcoinAttestations: [], errors };
|
||||
return { verified: false, targetDigest: targetDigestHex, attestations: parsed.attestations, bitcoinAttestations: [], trustMode: "structural-only", errors };
|
||||
}
|
||||
const verified = [];
|
||||
let trustMode = "structural-only";
|
||||
for (const att of bitcoinAttestations) {
|
||||
try {
|
||||
const blockHeader = await fetchBitcoinBlockHeader(att.height);
|
||||
if (blockHeader.trustMode === "multi-explorer-checked") {
|
||||
trustMode = "multi-explorer-checked";
|
||||
} else if (blockHeader.trustMode === "single-explorer-checked" && trustMode === "structural-only") {
|
||||
trustMode = "single-explorer-checked";
|
||||
}
|
||||
const computedMerkleRoot = bytesToHex3(att.digest.slice().reverse());
|
||||
if (computedMerkleRoot.toLowerCase() === blockHeader.merkleroot.toLowerCase()) {
|
||||
verified.push({
|
||||
@@ -10732,6 +10759,7 @@ async function verifyOtsProof(otsBytes, expectedDigestHex) {
|
||||
targetDigest: targetDigestHex,
|
||||
attestations: parsed.attestations,
|
||||
bitcoinAttestations: verified,
|
||||
trustMode,
|
||||
errors
|
||||
};
|
||||
}
|
||||
|
||||
+13
-3
@@ -640,8 +640,13 @@
|
||||
if (result.verified && result.bitcoinAttestations.length > 0) {
|
||||
const att = result.bitcoinAttestations[0];
|
||||
const date = new Date(att.time * 1000).toISOString().substring(0, 19);
|
||||
const trustLabel = result.trustMode === 'multi-explorer-checked'
|
||||
? 'multi-explorer-checked (cross-verified)'
|
||||
: result.trustMode === 'single-explorer-checked'
|
||||
? 'single-explorer-checked (trusted API)'
|
||||
: result.trustMode || 'unknown';
|
||||
setOtsBadge('pq-ots-badge-confirmed', 'OTS: Verified (Bitcoin)');
|
||||
setOtsInfo(`OpenTimestamps proof cryptographically verified. Bitcoin block ${att.height} (mined ${date} UTC). Merkle root matches. Proof commits to the event hash. Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`);
|
||||
setOtsInfo(`OpenTimestamps proof verified. Bitcoin block ${att.height} (mined ${date} UTC). Merkle root matches. Trust mode: ${trustLabel} — block header is trusted from explorer API(s), not independently verified against PoW. Proof commits to the event hash. Proof size: ${currentOtsBytes.length} bytes. ${hashMatchNote}.`);
|
||||
otsUpgradeBtn.style.display = 'none';
|
||||
} else {
|
||||
setOtsBadge('pq-ots-badge-none', 'OTS: Verification failed');
|
||||
@@ -853,11 +858,16 @@
|
||||
if (confirmed) {
|
||||
const att = verifyResult.bitcoinAttestations[0];
|
||||
const date = att ? new Date(att.time * 1000).toISOString().substring(0, 19) : 'unknown';
|
||||
setStatus(otsUpgradeStatus, 'success', `Bitcoin attestation verified! Block ${att ? att.height : '?'} (mined ${date} UTC). Proof upgraded (${currentOtsBytes.length} bytes). You can publish a new kind 9999 event carrying the confirmed proof below; it will reference the original via an upgrade_of tag.`);
|
||||
const trustLabel = verifyResult.trustMode === 'multi-explorer-checked'
|
||||
? 'multi-explorer-checked (cross-verified)'
|
||||
: verifyResult.trustMode === 'single-explorer-checked'
|
||||
? 'single-explorer-checked (trusted API)'
|
||||
: verifyResult.trustMode || 'unknown';
|
||||
setStatus(otsUpgradeStatus, 'success', `Bitcoin attestation verified! Block ${att ? att.height : '?'} (mined ${date} UTC). Trust mode: ${trustLabel}. Proof upgraded (${currentOtsBytes.length} bytes). You can publish a new kind 9999 event carrying the confirmed proof below; it will reference the original via an upgrade_of tag.`);
|
||||
otsUpgradeBtn.style.display = 'none';
|
||||
otsRepublishBtn.style.display = 'inline-block';
|
||||
setOtsBadge('pq-ots-badge-confirmed', 'OTS: Verified (Bitcoin)');
|
||||
setOtsInfo(`OpenTimestamps proof cryptographically verified. Bitcoin block ${att ? att.height : '?'}. Proof size: ${currentOtsBytes.length} bytes.`);
|
||||
setOtsInfo(`OpenTimestamps proof verified. Bitcoin block ${att ? att.height : '?'}. Trust mode: ${trustLabel}. Proof size: ${currentOtsBytes.length} bytes.`);
|
||||
} else {
|
||||
setStatus(otsUpgradeStatus, 'info', `Proof upgraded but still pending (no Bitcoin attestation yet). ${result.detail ? 'Detail: ' + result.detail : ''} Try again later.`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user