morphit/apps/indexer/scripts/treasury-mismatch-probe-smoke.ts

115 lines
5.5 KiB
TypeScript

/**
* treasury-mismatch-probe-smoke (cp316)
*
* Guards the federation treasury-address Mismatch detection: a peer
* instance that advertises a fee address DIFFERENT from the canonical
* (chain-pinned) treasury — i.e. an operator trying to redirect fee
* payments to their own pocket — must be flagged 'mismatch' on the
* /instances directory.
*
* LOGIC (treasuryMismatchReason — pure, the heart of the check):
* 1. matching addresses → no mismatch
* 2. peer advertises a DIFFERENT btc → mismatch (btc reason)
* 3. peer advertises a DIFFERENT xmr → mismatch (xmr reason)
* 4. peer OMITS treasury (older release) → no mismatch (back-compat)
* 5. peer advertises null (method disabled) → no mismatch (a
* legitimate operator choice, NOT a redirection)
* 6. no local canonical reference → no mismatch (skip)
* 7. per-chain null canonical → that chain skipped; other compared
*
* WIRING (static drift guards):
* 8. probeOne calls treasuryMismatchReason and returns mkMismatch
* 9. /v1/instance exposes the resolved `treasury` block
* 10. poller exposes currentTreasuryAddresses + feeds the scheduler
*/
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { treasuryMismatchReason } from '../src/indexer/federationProbe.ts';
const HERE = dirname(fileURLToPath(import.meta.url));
const REPO = join(HERE, '..', '..', '..');
let failures = 0;
let scenarios = 0;
const ok = (m: string) => {
console.log(`${m}`);
scenarios++;
};
const bad = (m: string, d: string) => {
console.error(`${m}\n ${d}`);
failures++;
scenarios++;
};
const BTC = 'bc1qdwaelg52ts3e0m8fellkw5u9x7plfwc0kxnwnk';
const XMR = '84bwu2PWp3NaRudAKTadmeZPBLTjL5f4bKU8F6NJKqxgUvwth6QxUVSUNFAQnHbbuQcMRNR4baYUKNcZXQtKMMKm4aVE3Fe';
const EVIL_BTC = 'bc1qevilevilevilevilevilevilevilevilxxxxxx';
const EVIL_XMR = '8EVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVILEVI';
const canon = { btc: BTC, xmr: XMR };
const expectNull = (label: string, r: string | null) =>
r === null ? ok(`${label} → no mismatch`) : bad(`${label} should NOT mismatch`, `got: ${r}`);
const expectMismatch = (label: string, r: string | null, needle: string) =>
r !== null && r.includes(needle)
? ok(`${label} → mismatch (${needle})`)
: bad(`${label} should mismatch on ${needle}`, `got: ${r}`);
// 1
expectNull('matching addresses', treasuryMismatchReason(canon, { btc: BTC, xmr: XMR }));
// 2
expectMismatch('peer redirects btc', treasuryMismatchReason(canon, { btc: EVIL_BTC, xmr: XMR }), 'treasury_btc_address');
// 3
expectMismatch('peer redirects xmr', treasuryMismatchReason(canon, { btc: BTC, xmr: EVIL_XMR }), 'treasury_xmr_address');
// 4
expectNull('peer omits treasury (older release)', treasuryMismatchReason(canon, undefined));
// 5
expectNull('peer disables btc (null)', treasuryMismatchReason(canon, { btc: null, xmr: XMR }));
expectNull('peer disables both (null)', treasuryMismatchReason(canon, { btc: null, xmr: null }));
// 6
expectNull('no local canonical reference', treasuryMismatchReason(null, { btc: EVIL_BTC, xmr: EVIL_XMR }));
// 7
expectNull(
'local btc disabled → btc skipped, xmr matches',
treasuryMismatchReason({ btc: null, xmr: XMR }, { btc: EVIL_BTC, xmr: XMR })
);
expectMismatch(
'local btc disabled → xmr still compared',
treasuryMismatchReason({ btc: null, xmr: XMR }, { btc: BTC, xmr: EVIL_XMR }),
'treasury_xmr_address'
);
// 8 — probeOne wiring
const probeSrc = readFileSync(join(REPO, 'apps/indexer/src/indexer/federationProbe.ts'), 'utf-8');
if (probeSrc.includes('treasuryMismatchReason(canonicalTreasury, instanceData.treasury)') && probeSrc.includes('return mkMismatch(treasuryReason)'))
ok('probeOne calls treasuryMismatchReason and returns mkMismatch on divergence');
else bad('probeOne no longer wires the treasury mismatch check', 'redirection would go unflagged');
if (probeSrc.includes('probeOne(inst, this.config.canonicalTreasury?.()'))
ok('probePool threads the canonical treasury into probeOne');
else bad('probePool no longer passes canonicalTreasury to probeOne', 'probe would compare against nothing');
// 9 — /v1/instance exposure
const instSrc = readFileSync(join(REPO, 'apps/indexer/src/api/instance.ts'), 'utf-8');
if (instSrc.includes('treasury: getTreasuryAddresses()') && /treasury:\s*\{\s*btc:/.test(instSrc))
ok('/v1/instance exposes the resolved treasury block');
else bad('/v1/instance no longer exposes treasury', 'peers cannot audit the address → no mismatch detection');
// 10 — poller wiring
const pollerSrc = readFileSync(join(REPO, 'apps/indexer/src/indexer/poller.ts'), 'utf-8');
if (pollerSrc.includes('currentTreasuryAddresses()') && pollerSrc.includes('canonicalTreasury: () => this.currentTreasuryAddresses()'))
ok('poller exposes currentTreasuryAddresses + feeds it to the probe scheduler');
else bad('poller no longer feeds the resolved treasury to the probe', 'canonical reference lost');
const mainSrc = readFileSync(join(REPO, 'apps/indexer/src/main.ts'), 'utf-8');
if (mainSrc.includes('instanceRoute(config, () => poller.currentTreasuryAddresses())'))
ok('main.ts wires the resolved treasury getter into /v1/instance');
else bad('main.ts no longer wires the treasury getter', '/v1/instance treasury would be null');
console.log(`\n${'─'.repeat(54)}`);
if (failures === 0) {
console.log(`✓ all ${scenarios} scenarios passed`);
process.exit(0);
} else {
console.log(`${failures}/${scenarios} scenarios failed`);
process.exit(1);
}