166 lines
6.9 KiB
TypeScript
166 lines
6.9 KiB
TypeScript
#!/usr/bin/env tsx
|
|
/**
|
|
* scripts/witness-chat-identity-claimed-op-smoke.ts
|
|
*
|
|
* v1.8.15 (cp555) — witness chat-identity verification structural guard.
|
|
*
|
|
* THE BUG THIS LOCKS IN THE FIX FOR. Ken and @mariuszkarowski could not open a
|
|
* chat with the Blurt witness @khrom: every send died with a red "tamper
|
|
* detected — the blockchain reports none" banner (pub_pin_chain_reports_none),
|
|
* while the two of them chatted with each other fine. Root cause: the chain
|
|
* check found a peer's `morphit_chat_identity_v1` op by WALKING account history
|
|
* (get_account_history, 10000-entry per-call cap). For a block producer that op
|
|
* is buried under ~1,430 `producer_reward` virtual ops PER DAY — beyond any
|
|
* single window (10000 ≈ a week of a witness's activity). The walk found
|
|
* nothing → null → false tamper.
|
|
*
|
|
* THE FIX. Verify the indexer's CLAIMED op directly by trx_id
|
|
* (verifyClaimedChatIdentityOnChain → chainRelay('get_transaction', …)): O(1),
|
|
* immune to account activity. The chat send path (chatService) and the OOB
|
|
* fingerprint path (peerPubFetch) both call verifyPeerChatIdentityOnChain,
|
|
* which tries the claimed op first and keeps the history walk only as a
|
|
* fallback.
|
|
*
|
|
* WHAT THIS SMOKE ASSERTS (structural — fails at lint time if the wiring
|
|
* regresses back to a bare history walk at the call sites):
|
|
* 1. chainVerify exports verifyClaimedChatIdentityOnChain + verifyPeerChatIdentityOnChain.
|
|
* 2. the claimed-op verifier fetches the op via get_transaction (the O(1) path).
|
|
* 3. the orchestrator tries the claimed op first (calls verifyClaimedChatIdentityOnChain).
|
|
* 4. the two call sites (chatService, peerPubFetch) wire verifyPeerChatIdentityOnChain
|
|
* and no longer reference the raw history-walk (fetchLatestChatIdentityFromChainQuorum).
|
|
* 5. pubPin threads the indexer's claimed ref into verifyOnChain (so the primary
|
|
* path can chase the exact op instead of re-deriving "the latest").
|
|
*
|
|
* COMMENTS ARE STRIPPED BEFORE GREPPING. This guard's evidence is the CODE, not
|
|
* the prose — and the fix's own comments necessarily NAME the retired history
|
|
* walk (`fetchLatestChatIdentityFromChainQuorum`) and the buried-op problem. A
|
|
* raw grep would trip on those comments. The blunt stripper can only ever
|
|
* REMOVE evidence, so its failure mode is a false ALARM (noisy, fixed), never a
|
|
* false pass.
|
|
*/
|
|
|
|
import { readFileSync } from 'node:fs';
|
|
import { dirname, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const REPO = resolve(HERE, '..');
|
|
|
|
/** Blunt comment strip (block + line, with a `:` guard so `https://` survives). */
|
|
const stripComments = (src: string): string =>
|
|
src.replace(/\/\*[\s\S]*?\*\//g, ' ').replace(/(^|[^:])\/\/[^\n]*/g, '$1');
|
|
|
|
function readStripped(rel: string): string {
|
|
return stripComments(readFileSync(resolve(REPO, rel), 'utf8'));
|
|
}
|
|
|
|
interface Scenario {
|
|
name: string;
|
|
ok: boolean;
|
|
detail?: string;
|
|
}
|
|
|
|
const results: Scenario[] = [];
|
|
function check(name: string, ok: boolean, detail?: string): void {
|
|
results.push({ name, ok, detail });
|
|
}
|
|
|
|
console.log('\n── witness chat-identity claimed-op structural smoke ───\n');
|
|
|
|
const CHAIN_VERIFY = 'apps/web/src/lib/chat/chainVerify.ts';
|
|
const PUB_PIN = 'apps/web/src/lib/chat/pubPin.ts';
|
|
const CHAT_SERVICE = 'apps/web/src/lib/chat/chatService.ts';
|
|
const PEER_PUB_FETCH = 'apps/web/src/lib/chat/peerPubFetch.ts';
|
|
|
|
const chainVerify = readStripped(CHAIN_VERIFY);
|
|
const pubPin = readStripped(PUB_PIN);
|
|
const chatService = readStripped(CHAT_SERVICE);
|
|
const peerPubFetch = readStripped(PEER_PUB_FETCH);
|
|
|
|
// 1. chainVerify exports both new functions.
|
|
check(
|
|
'chainVerify exports verifyClaimedChatIdentityOnChain',
|
|
/export\s+async\s+function\s+verifyClaimedChatIdentityOnChain\b/.test(chainVerify),
|
|
CHAIN_VERIFY
|
|
);
|
|
check(
|
|
'chainVerify exports verifyPeerChatIdentityOnChain',
|
|
/export\s+async\s+function\s+verifyPeerChatIdentityOnChain\b/.test(chainVerify),
|
|
CHAIN_VERIFY
|
|
);
|
|
|
|
// 2. The claimed-op verifier fetches the op via get_transaction (O(1) path).
|
|
// Scoped to the function body so we credit the real call, not a stray token.
|
|
const claimedFnMatch = chainVerify.match(
|
|
/verifyClaimedChatIdentityOnChain[\s\S]*?\n\}/
|
|
);
|
|
const claimedFnBody = claimedFnMatch ? claimedFnMatch[0] : '';
|
|
check(
|
|
'verifyClaimedChatIdentityOnChain fetches the claimed op via get_transaction',
|
|
/chainRelay\s*(<[^>]*>)?\s*\(\s*['"]get_transaction['"]/.test(claimedFnBody),
|
|
CHAIN_VERIFY
|
|
);
|
|
check(
|
|
'verifyClaimedChatIdentityOnChain verifies signature (verifyTransactionSignatures)',
|
|
/verifyTransactionSignatures\s*\(/.test(claimedFnBody),
|
|
CHAIN_VERIFY
|
|
);
|
|
|
|
// 3. The orchestrator tries the claimed op first.
|
|
const orchMatch = chainVerify.match(/verifyPeerChatIdentityOnChain[\s\S]*?\n\}/g);
|
|
const orchBody = orchMatch ? orchMatch[orchMatch.length - 1] : '';
|
|
check(
|
|
'verifyPeerChatIdentityOnChain tries the claimed op first',
|
|
/verifyClaimedChatIdentityOnChain\s*\(/.test(orchBody),
|
|
CHAIN_VERIFY
|
|
);
|
|
|
|
// 4. Both call sites wire the claimed-op orchestrator and no longer reference
|
|
// the raw history walk directly (that walk survives ONLY inside chainVerify
|
|
// as the fallback).
|
|
check(
|
|
'chatService wires verifyPeerChatIdentityOnChain',
|
|
/verifyPeerChatIdentityOnChain\s*\(/.test(chatService),
|
|
CHAT_SERVICE
|
|
);
|
|
check(
|
|
'chatService no longer references the raw history walk',
|
|
!chatService.includes('fetchLatestChatIdentityFromChainQuorum'),
|
|
`${CHAT_SERVICE} still mentions fetchLatestChatIdentityFromChainQuorum (should route through verifyPeerChatIdentityOnChain)`
|
|
);
|
|
check(
|
|
'peerPubFetch wires verifyPeerChatIdentityOnChain',
|
|
/verifyPeerChatIdentityOnChain\s*\(/.test(peerPubFetch),
|
|
PEER_PUB_FETCH
|
|
);
|
|
check(
|
|
'peerPubFetch no longer references the raw history walk',
|
|
!peerPubFetch.includes('fetchLatestChatIdentityFromChainQuorum'),
|
|
`${PEER_PUB_FETCH} still mentions fetchLatestChatIdentityFromChainQuorum`
|
|
);
|
|
|
|
// 5. pubPin threads the indexer's claimed ref into verifyOnChain, so the
|
|
// primary path can chase the exact op rather than re-deriving "the latest".
|
|
check(
|
|
'pubPin passes the claimed ref into verifyOnChain',
|
|
/verifyOnChain\s*\(\s*peer\s*,\s*indexerPin\s*\)/.test(pubPin),
|
|
PUB_PIN
|
|
);
|
|
check(
|
|
'pubPin verifyOnChain signature accepts a claimed ref',
|
|
/verifyOnChain:\s*\(\s*peer:\s*string\s*,\s*claimedRef:\s*ChatPubPin/.test(pubPin),
|
|
PUB_PIN
|
|
);
|
|
|
|
// ── report ─────────────────────────────────────────────
|
|
const failed = results.filter((r) => !r.ok);
|
|
for (const r of results) {
|
|
console.log(` ${r.ok ? '✓' : '✗'} ${r.name}`);
|
|
if (!r.ok && r.detail) console.log(` ${r.detail}`);
|
|
}
|
|
console.log('\n──────────────────────────────────────────────────────');
|
|
if (failed.length > 0) {
|
|
console.log(`✗ ${failed.length}/${results.length} scenarios failed`);
|
|
process.exit(1);
|
|
}
|
|
console.log(`✓ all ${results.length} scenarios passed`);
|