morphit/apps/indexer/scripts/release-validator-smoke.ts

883 lines
34 KiB
TypeScript

/**
* Morphit smoke — release-payload validator (Batch J).
*
* Pure logic. Mirrors the indexer's rejection reasons so any
* release the indexer stores as valid passes here too.
*/
import { validateReleasePayload } from '@morphit/release-schema';
import { checkPinnedKeyInAuthority } from '@morphit/release-schema';
let scenarios = 0;
let failures = 0;
function scenario(name: string, fn: () => void): void {
scenarios++;
try {
fn();
console.log(`${name}`);
} catch (err) {
failures++;
console.log(`${name}: ${err instanceof Error ? err.message : String(err)}`);
}
}
console.log('\n── release validator smoke ───────────────────────────────\n');
const VALID_HASH = 'sha256-' + 'a'.repeat(43) + '=';
const VALID_HASH_2 = 'sha256-' + 'b'.repeat(43) + '=';
function makePayload(over: Partial<Record<string, unknown>> = {}): unknown {
return {
version: '1.2.3',
hash_manifest: { 'index.html': VALID_HASH },
endpoints: { indexer: ['https://idx.example.com'] },
...over
};
}
// ─── happy path ─────────────────────────────────────────────────────
scenario('accepts a minimal valid payload', () => {
const r = validateReleasePayload(makePayload());
if (!r.ok) throw new Error(`got ${r.reason}`);
if (r.value.version !== '1.2.3') throw new Error('version');
});
scenario('accepts pre-release semver', () => {
const r = validateReleasePayload(makePayload({ version: '1.0.0-rc.1' }));
if (!r.ok) throw new Error(`got ${r.reason}`);
});
scenario('accepts build-metadata semver', () => {
const r = validateReleasePayload(makePayload({ version: '1.0.0+build.42' }));
if (!r.ok) throw new Error(`got ${r.reason}`);
});
scenario('accepts empty endpoints object', () => {
const r = validateReleasePayload(makePayload({ endpoints: {} }));
if (!r.ok) throw new Error(`got ${r.reason}`);
});
scenario('cp436 — accepts a payload with NO endpoints field (no longer pinned)', () => {
const p = makePayload() as Record<string, unknown>;
delete p.endpoints;
const r = validateReleasePayload(p);
if (!r.ok) throw new Error(`got ${r.reason}`);
if ('endpoints' in r.value) throw new Error('endpoints must be omitted from output when absent');
});
scenario('cp436 — still accepts a payload WITH endpoints (backward-compat)', () => {
const r = validateReleasePayload(makePayload({ endpoints: { indexer: ['https://idx.example.com'] } }));
if (!r.ok) throw new Error(`got ${r.reason}`);
if (!r.value.endpoints) throw new Error('endpoints should be preserved when present');
});
scenario('accepts optional signature', () => {
const r = validateReleasePayload(makePayload({ signature: 'pgp-sig-blob' }));
if (!r.ok) throw new Error(`got ${r.reason}`);
if (r.value.signature !== 'pgp-sig-blob') throw new Error('sig');
});
scenario('drops signature when undefined', () => {
const r = validateReleasePayload(makePayload());
if (!r.ok) throw new Error('expected ok');
if (r.value.signature !== undefined) throw new Error('expected undefined');
});
// ─── payload shape ──────────────────────────────────────────────────
scenario('rejects null payload', () => {
const r = validateReleasePayload(null);
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'payload_not_object') throw new Error(r.reason);
});
scenario('rejects array payload', () => {
const r = validateReleasePayload([1, 2, 3]);
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'payload_not_object') throw new Error(r.reason);
});
scenario('rejects string payload', () => {
const r = validateReleasePayload('hello');
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'payload_not_object') throw new Error(r.reason);
});
// ─── version ────────────────────────────────────────────────────────
scenario('rejects non-string version', () => {
const r = validateReleasePayload(makePayload({ version: 123 }));
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'version_not_string') throw new Error(r.reason);
});
scenario('rejects non-semver version', () => {
const r = validateReleasePayload(makePayload({ version: 'v1.0' }));
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'version_not_semver') throw new Error(r.reason);
});
scenario('rejects empty version', () => {
const r = validateReleasePayload(makePayload({ version: '' }));
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'version_not_semver') throw new Error(r.reason);
});
// ─── hash_manifest ──────────────────────────────────────────────────
scenario('rejects non-object hash_manifest', () => {
const r = validateReleasePayload(makePayload({ hash_manifest: 'foo' }));
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'hash_manifest_not_object') throw new Error(r.reason);
});
scenario('rejects array hash_manifest', () => {
const r = validateReleasePayload(makePayload({ hash_manifest: [] }));
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'hash_manifest_not_object') throw new Error(r.reason);
});
scenario('rejects non-SRI-format hash value', () => {
const r = validateReleasePayload(makePayload({ hash_manifest: { 'index.html': 'not-a-hash' } }));
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'hash_manifest_entry_invalid') throw new Error(r.reason);
});
scenario('rejects non-string hash value', () => {
const r = validateReleasePayload(makePayload({ hash_manifest: { 'index.html': 42 } }));
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'hash_manifest_entry_invalid') throw new Error(r.reason);
});
scenario('rejects md5-prefixed hash', () => {
const r = validateReleasePayload(
makePayload({ hash_manifest: { a: 'md5-' + 'a'.repeat(43) + '=' } })
);
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'hash_manifest_entry_invalid') throw new Error(r.reason);
});
scenario('rejects huge hash_manifest', () => {
const big: Record<string, string> = {};
for (let i = 0; i < 1000; i++) {
big[`asset/${i}/${'x'.repeat(100)}`] = VALID_HASH;
}
const r = validateReleasePayload(makePayload({ hash_manifest: big }));
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'hash_manifest_too_large') throw new Error(r.reason);
});
scenario('accepts empty hash_manifest', () => {
const r = validateReleasePayload(makePayload({ hash_manifest: {} }));
if (!r.ok) throw new Error(`got ${r.reason}`);
});
scenario('accepts manifest with multiple valid entries', () => {
const r = validateReleasePayload(
makePayload({
hash_manifest: {
'index.html': VALID_HASH,
'_app/start.js': VALID_HASH_2,
'_app/style.css': VALID_HASH
}
})
);
if (!r.ok) throw new Error(`got ${r.reason}`);
});
// ─── endpoints ──────────────────────────────────────────────────────
scenario('rejects non-object endpoints', () => {
const r = validateReleasePayload(makePayload({ endpoints: 'foo' }));
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'endpoints_not_object') throw new Error(r.reason);
});
scenario('rejects non-array endpoint list', () => {
const r = validateReleasePayload(
makePayload({ endpoints: { indexer: 'https://idx.example.com' } })
);
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'endpoints_entry_invalid') throw new Error(r.reason);
});
scenario('rejects http (non-https) endpoint URL', () => {
const r = validateReleasePayload(
makePayload({ endpoints: { indexer: ['http://idx.example.com'] } })
);
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'endpoints_entry_invalid') throw new Error(r.reason);
});
scenario('rejects endpoint URL with whitespace', () => {
const r = validateReleasePayload(
makePayload({ endpoints: { indexer: ['https://idx.example.com path'] } })
);
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'endpoints_entry_invalid') throw new Error(r.reason);
});
scenario('rejects javascript: URL', () => {
const r = validateReleasePayload(
makePayload({ endpoints: { indexer: ['javascript:alert(1)'] } })
);
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'endpoints_entry_invalid') throw new Error(r.reason);
});
scenario('rejects too-long endpoint URL', () => {
const r = validateReleasePayload(
makePayload({
endpoints: {
indexer: ['https://example.com/' + 'a'.repeat(300)]
}
})
);
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'endpoints_entry_invalid') throw new Error(r.reason);
});
scenario('accepts multiple endpoint tiers', () => {
const r = validateReleasePayload(
makePayload({
endpoints: {
relay: ['https://relay-a.example.com', 'https://relay-b.example.com'],
indexer: ['https://idx.example.com'],
avatar: ['https://av.example.com'],
blurt: ['https://rpc.blurt.blog']
}
})
);
if (!r.ok) throw new Error(`got ${r.reason}`);
});
// ─── signature ──────────────────────────────────────────────────────
scenario('rejects non-string signature', () => {
const r = validateReleasePayload(makePayload({ signature: 42 }));
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'signature_not_string') throw new Error(r.reason);
});
scenario('rejects too-long signature', () => {
const r = validateReleasePayload(makePayload({ signature: 'x'.repeat(513) }));
if (r.ok) throw new Error('should have failed');
if (r.reason !== 'signature_too_long') throw new Error(r.reason);
});
scenario('treats null signature as omitted', () => {
const r = validateReleasePayload(makePayload({ signature: null }));
if (!r.ok) throw new Error(`got ${r.reason}`);
if (r.value.signature !== undefined) throw new Error('expected undefined');
});
// ─── checkPinnedKeyInAuthority (audit J-1) ──────────────────────────
const PIN = 'BLT5RKpMmcjhZJfZbCuvm9eBmZxBcoG6m6gw2L1mwo7vT8wFBfXMR';
const OTHER = 'BLT8XVuDwDQzEqFP9oo7nLaG7tZpkGqSLPVpZGoa7vH2tuFtdg6jL';
scenario('checkPinnedKeyInAuthority: pinned at weight 1 → ok', () => {
const r = checkPinnedKeyInAuthority([[PIN, 1]], PIN);
if (!r.ok) throw new Error('expected ok');
if (r.pinnedWeight !== 1) throw new Error(`weight=${r.pinnedWeight}`);
if (r.chainKeys.length !== 1) throw new Error('chainKeys');
});
scenario('checkPinnedKeyInAuthority: pinned at weight 0 → REJECT (J-1)', () => {
const r = checkPinnedKeyInAuthority([[PIN, 0]], PIN);
if (r.ok) throw new Error('weight-0 entry should not pass');
if (r.pinnedWeight !== 0) throw new Error(`weight=${r.pinnedWeight}`);
});
scenario('checkPinnedKeyInAuthority: pinned absent → reject', () => {
const r = checkPinnedKeyInAuthority([[OTHER, 1]], PIN);
if (r.ok) throw new Error('absent should not pass');
if (!r.chainKeys.includes(OTHER)) throw new Error('chainKeys');
});
scenario('checkPinnedKeyInAuthority: pinned alongside others → ok', () => {
const r = checkPinnedKeyInAuthority(
[
[OTHER, 1],
[PIN, 2]
],
PIN
);
if (!r.ok) throw new Error('expected ok');
if (r.pinnedWeight !== 2) throw new Error(`weight=${r.pinnedWeight}`);
});
scenario('checkPinnedKeyInAuthority: pinned weight-0 alongside hostile weight-1 → REJECT', () => {
// The exact attack J-1 closes: attacker rotated the live key
// to OTHER but left PIN listed at weight 0 to fool a naive
// "is the pin present" check.
const r = checkPinnedKeyInAuthority(
[
[OTHER, 1],
[PIN, 0]
],
PIN
);
if (r.ok) throw new Error('attack scenario should be caught');
});
scenario('checkPinnedKeyInAuthority: malformed entry tolerated', () => {
// Hostile RPC returns a key_auths entry that's not a [str,
// num] tuple. Helper should ignore it gracefully and look at
// the rest of the array.
const r = checkPinnedKeyInAuthority([null, 'foo', [PIN, 1], [42, 1]], PIN);
if (!r.ok) throw new Error('expected ok despite garbage');
});
scenario('checkPinnedKeyInAuthority: non-array key_auths → reject', () => {
const r = checkPinnedKeyInAuthority('not an array', PIN);
if (r.ok) throw new Error('expected reject');
if (r.chainKeys.length !== 0) throw new Error('chainKeys');
});
scenario('checkPinnedKeyInAuthority: undefined key_auths → reject', () => {
const r = checkPinnedKeyInAuthority(undefined, PIN);
if (r.ok) throw new Error('expected reject');
});
// ─── checkPinnedKeyInAuthority (audit J-1) ──────────────────────────
scenario('checkPinnedKeyInAuthority: pinned at weight 1 → ok', () => {
const r = checkPinnedKeyInAuthority([[PIN, 1]], PIN);
if (!r.ok) throw new Error('expected ok');
if (r.pinnedWeight !== 1) throw new Error(`weight=${r.pinnedWeight}`);
if (r.chainKeys.length !== 1) throw new Error('chainKeys');
});
scenario('checkPinnedKeyInAuthority: pinned at weight 0 → REJECT (J-1)', () => {
const r = checkPinnedKeyInAuthority([[PIN, 0]], PIN);
if (r.ok) throw new Error('weight-0 entry should not pass');
if (r.pinnedWeight !== 0) throw new Error(`weight=${r.pinnedWeight}`);
});
scenario('checkPinnedKeyInAuthority: pinned absent → reject', () => {
const r = checkPinnedKeyInAuthority([[OTHER, 1]], PIN);
if (r.ok) throw new Error('absent should not pass');
if (!r.chainKeys.includes(OTHER)) throw new Error('chainKeys');
});
scenario('checkPinnedKeyInAuthority: pinned alongside others → ok', () => {
const r = checkPinnedKeyInAuthority(
[
[OTHER, 1],
[PIN, 2]
],
PIN
);
if (!r.ok) throw new Error('expected ok');
if (r.pinnedWeight !== 2) throw new Error(`weight=${r.pinnedWeight}`);
});
scenario('checkPinnedKeyInAuthority: pinned weight-0 alongside hostile weight-1 → REJECT', () => {
// The exact attack J-1 closes: attacker rotated the live key
// to OTHER but left PIN listed at weight 0 to fool a naive
// "is the pin present" check.
const r = checkPinnedKeyInAuthority(
[
[OTHER, 1],
[PIN, 0]
],
PIN
);
if (r.ok) throw new Error('attack scenario should be caught');
});
scenario('checkPinnedKeyInAuthority: malformed entry tolerated', () => {
// Hostile RPC returns a key_auths entry that's not a [str,
// num] tuple. Helper should ignore it gracefully and look at
// the rest of the array.
const r = checkPinnedKeyInAuthority([null, 'foo', [PIN, 1], [42, 1]], PIN);
if (!r.ok) throw new Error('expected ok despite garbage');
});
scenario('checkPinnedKeyInAuthority: non-array key_auths → reject', () => {
const r = checkPinnedKeyInAuthority('not an array', PIN);
if (r.ok) throw new Error('expected reject');
if (r.chainKeys.length !== 0) throw new Error('chainKeys');
});
scenario('checkPinnedKeyInAuthority: undefined key_auths → reject', () => {
const r = checkPinnedKeyInAuthority(undefined, PIN);
if (r.ok) throw new Error('expected reject');
});
// ─── Part 106 — treasury chain-pin validation ────────────────────────
//
// These scenarios mirror the indexer-side validateTreasury() rules
// in apps/indexer/src/indexer/handlers/release.ts. Any treasury
// payload the indexer rejects must also be rejected here, with
// matching reason names.
const VALID_BTC_ADDR = 'bc1q' + 'a'.repeat(38); // bech32, 42 chars total
const VALID_XMR_ADDR = '4' + 'A'.repeat(94);
const VALID_XMR_VK = 'a'.repeat(64);
function withTreasury(t: unknown): unknown {
return makePayload({ treasury: t });
}
scenario('Part 106: missing treasury → ok (back-compat)', () => {
const r = validateReleasePayload(makePayload());
if (!r.ok) throw new Error('expected ok without treasury');
if (r.value.treasury !== undefined) throw new Error('treasury should be undefined when absent');
});
scenario('Part 106: treasury=null → ok (explicit absent)', () => {
const r = validateReleasePayload(withTreasury(null));
if (!r.ok) throw new Error('expected ok');
if (r.value.treasury !== undefined) throw new Error('null should normalize to undefined');
});
scenario('Part 106: treasury={btc:null,xmr:null} → ok (no pin yet)', () => {
const r = validateReleasePayload(withTreasury({ btc: null, xmr: null }));
if (!r.ok) throw new Error('expected ok');
if (r.value.treasury?.btc !== null || r.value.treasury?.xmr !== null) {
throw new Error('expected both nulls preserved');
}
});
scenario('Part 106: treasury not object → reject', () => {
const r = validateReleasePayload(withTreasury('not an object'));
if (r.ok || r.reason !== 'treasury_not_object') throw new Error(`expected treasury_not_object, got ${r.ok ? 'ok' : r.reason}`);
});
scenario('Part 106: BTC valid bech32 → ok', () => {
const r = validateReleasePayload(withTreasury({
btc: { address: VALID_BTC_ADDR, satoshis: 416 }, xmr: null
}));
if (!r.ok) throw new Error('expected ok');
if (r.value.treasury?.btc?.address !== VALID_BTC_ADDR) throw new Error('btc address not preserved');
});
scenario('Part 106: BTC testnet (tb1) → reject', () => {
const r = validateReleasePayload(withTreasury({
btc: { address: 'tb1q' + 'a'.repeat(38), satoshis: 416 }, xmr: null
}));
if (r.ok || r.reason !== 'treasury_btc_address_not_mainnet') {
throw new Error(`expected treasury_btc_address_not_mainnet, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('Part 106: BTC empty address → reject', () => {
const r = validateReleasePayload(withTreasury({
btc: { address: '', satoshis: 416 }, xmr: null
}));
if (r.ok || r.reason !== 'treasury_btc_address_missing') {
throw new Error(`expected treasury_btc_address_missing, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('Part 106: BTC zero satoshis → reject', () => {
const r = validateReleasePayload(withTreasury({
btc: { address: VALID_BTC_ADDR, satoshis: 0 }, xmr: null
}));
if (r.ok || r.reason !== 'treasury_btc_satoshis_invalid') {
throw new Error(`expected treasury_btc_satoshis_invalid, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('Part 106: BTC negative satoshis → reject', () => {
const r = validateReleasePayload(withTreasury({
btc: { address: VALID_BTC_ADDR, satoshis: -1 }, xmr: null
}));
if (r.ok || r.reason !== 'treasury_btc_satoshis_invalid') {
throw new Error(`expected treasury_btc_satoshis_invalid, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('Part 106: BTC non-integer satoshis → reject', () => {
const r = validateReleasePayload(withTreasury({
btc: { address: VALID_BTC_ADDR, satoshis: 1.5 }, xmr: null
}));
if (r.ok || r.reason !== 'treasury_btc_satoshis_invalid') {
throw new Error(`expected treasury_btc_satoshis_invalid, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('Part 106: BTC absurdly large satoshis → reject', () => {
// 1000 BTC per listing = 100_000_000_000 sats; ceiling is exclusive at that.
const r = validateReleasePayload(withTreasury({
btc: { address: VALID_BTC_ADDR, satoshis: 100_000_000_001 }, xmr: null
}));
if (r.ok || r.reason !== 'treasury_btc_satoshis_too_large') {
throw new Error(`expected treasury_btc_satoshis_too_large, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('Part 106 + 107: XMR primary address (4...), no viewkey field → ok', () => {
const r = validateReleasePayload(withTreasury({
btc: null, xmr: { address: VALID_XMR_ADDR, piconero: '781250000' }
}));
if (!r.ok) throw new Error('expected ok');
// Part 107 invariant: validator output MUST NOT contain a viewkey field.
if (r.value.treasury?.xmr !== null && 'viewkey' in (r.value.treasury?.xmr ?? {})) {
throw new Error('Part 107 invariant violated: validator output contains viewkey');
}
});
scenario('Part 106 + 107: XMR subaddress (8...), no viewkey field → ok', () => {
const subaddr = '8' + 'A'.repeat(94);
const r = validateReleasePayload(withTreasury({
btc: null, xmr: { address: subaddr, piconero: '781250000' }
}));
if (!r.ok) throw new Error('expected ok');
});
scenario('Part 106 + 107: XMR testnet (9...) → reject', () => {
const r = validateReleasePayload(withTreasury({
btc: null, xmr: { address: '9' + 'A'.repeat(94), piconero: '781250000' }
}));
if (r.ok || r.reason !== 'treasury_xmr_address_not_mainnet') {
throw new Error(`expected treasury_xmr_address_not_mainnet, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('Part 107: XMR with viewkey field PRESENT → silently ignored, output STRIPS it', () => {
// A release op broadcast before Part 107 (or by a hostile
// operator who hand-crafted a payload) might include a
// viewkey field. Part 107 says: never reject for that
// (we don't want to break parsing of pre-Part-107 ops),
// but DO strip the field — the validated output must not
// carry a viewkey under any circumstance.
const r = validateReleasePayload(withTreasury({
btc: null,
xmr: {
address: VALID_XMR_ADDR,
viewkey: VALID_XMR_VK, // present in input
piconero: '781250000'
}
}));
if (!r.ok) throw new Error('expected ok (viewkey is silently ignored, not rejected)');
const xmr = r.value.treasury?.xmr;
if (xmr === null || xmr === undefined) throw new Error('expected xmr to be populated');
if ('viewkey' in xmr) {
throw new Error('Part 107 invariant violated: viewkey field present in validator output');
}
});
scenario('Part 107: XMR with garbage viewkey field → still silently ignored, no rejection', () => {
// Even if the input viewkey is malformed, the validator must
// not reject — the field is no longer part of validation.
// Defense-in-depth: ensures we don't leak the existence of
// the viewkey field to attackers via differential rejection
// reasons.
const r = validateReleasePayload(withTreasury({
btc: null,
xmr: {
address: VALID_XMR_ADDR,
viewkey: 'absolutely-not-a-viewkey',
piconero: '781250000'
}
}));
if (!r.ok) throw new Error(`expected ok regardless of viewkey field, got ${r.reason}`);
});
scenario('Part 106 + 107: XMR piconero "0" → reject', () => {
const r = validateReleasePayload(withTreasury({
btc: null, xmr: { address: VALID_XMR_ADDR, piconero: '0' }
}));
if (r.ok || r.reason !== 'treasury_xmr_piconero_invalid') {
throw new Error(`expected treasury_xmr_piconero_invalid, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('Part 106 + 107: XMR piconero non-numeric → reject', () => {
const r = validateReleasePayload(withTreasury({
btc: null, xmr: { address: VALID_XMR_ADDR, piconero: '1e9' }
}));
if (r.ok || r.reason !== 'treasury_xmr_piconero_invalid') {
throw new Error(`expected treasury_xmr_piconero_invalid, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('Part 106 + 107: XMR piconero 17-digit → reject (size cap)', () => {
const r = validateReleasePayload(withTreasury({
btc: null, xmr: { address: VALID_XMR_ADDR, piconero: '1'.repeat(17) }
}));
if (r.ok || r.reason !== 'treasury_xmr_piconero_too_large') {
throw new Error(`expected treasury_xmr_piconero_too_large, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('Part 106 + 107: both BTC and XMR pinned → ok, output has no viewkey', () => {
const r = validateReleasePayload(withTreasury({
btc: { address: VALID_BTC_ADDR, satoshis: 416 },
xmr: { address: VALID_XMR_ADDR, piconero: '781250000' }
}));
if (!r.ok) throw new Error('expected ok');
if (r.value.treasury?.btc?.address !== VALID_BTC_ADDR) throw new Error('btc');
if (r.value.treasury?.xmr?.address !== VALID_XMR_ADDR) throw new Error('xmr');
// Part 107 invariant.
const xmrOut = r.value.treasury?.xmr;
if (xmrOut !== null && xmrOut !== undefined && 'viewkey' in xmrOut) {
throw new Error('Part 107 invariant violated');
}
});
scenario('Part 106: BTC legacy address (1...) → ok', () => {
const legacy = '1' + 'A'.repeat(33); // 34 chars total, mainnet legacy
const r = validateReleasePayload(withTreasury({
btc: { address: legacy, satoshis: 416 }, xmr: null
}));
if (!r.ok) throw new Error('expected ok');
});
scenario('Part 106: BTC P2SH address (3...) → ok', () => {
const p2sh = '3' + 'A'.repeat(33);
const r = validateReleasePayload(withTreasury({
btc: { address: p2sh, satoshis: 416 }, xmr: null
}));
if (!r.ok) throw new Error('expected ok');
});
scenario('Part 106: BTC legacy testnet (m...) → reject', () => {
const r = validateReleasePayload(withTreasury({
btc: { address: 'm' + 'A'.repeat(33), satoshis: 416 }, xmr: null
}));
if (r.ok || r.reason !== 'treasury_btc_address_not_mainnet') {
throw new Error(`expected reject, got ${r.ok ? 'ok' : r.reason}`);
}
});
// ─── cp372 — chain-pinned BLURT fee base ─────────────────────────────
scenario('cp372: treasury.blurt absent → ok (back-compat, no BLURT pin)', () => {
const r = validateReleasePayload(withTreasury({ btc: null, xmr: null }));
if (!r.ok) throw new Error('expected ok without blurt');
if (r.value.treasury?.blurt !== undefined) {
throw new Error('blurt should be absent when not provided (byte-identical to pre-cp372)');
}
});
scenario('cp372: treasury.blurt={base:62.5} → ok, preserved', () => {
const r = validateReleasePayload(withTreasury({ btc: null, xmr: null, blurt: { base: 62.5 } }));
if (!r.ok) throw new Error('expected ok');
if (r.value.treasury?.blurt?.base !== 62.5) throw new Error('blurt base not preserved');
});
scenario('cp372: treasury.blurt=null → ok (explicit no-pin)', () => {
const r = validateReleasePayload(withTreasury({ btc: null, xmr: null, blurt: null }));
if (!r.ok) throw new Error('expected ok');
if (r.value.treasury?.blurt !== undefined) throw new Error('null blurt normalizes to absent');
});
scenario('cp372: blurt not object → reject', () => {
const r = validateReleasePayload(withTreasury({ btc: null, xmr: null, blurt: 62.5 as unknown as { base: number } }));
if (r.ok || r.reason !== 'treasury_blurt_not_object') {
throw new Error(`expected treasury_blurt_not_object, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('cp372: blurt base zero → reject', () => {
const r = validateReleasePayload(withTreasury({ btc: null, xmr: null, blurt: { base: 0 } }));
if (r.ok || r.reason !== 'treasury_blurt_base_invalid') {
throw new Error(`expected treasury_blurt_base_invalid, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('cp372: blurt base negative → reject', () => {
const r = validateReleasePayload(withTreasury({ btc: null, xmr: null, blurt: { base: -5 } }));
if (r.ok || r.reason !== 'treasury_blurt_base_invalid') {
throw new Error(`expected treasury_blurt_base_invalid, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('cp372: blurt base non-finite → reject', () => {
const r = validateReleasePayload(withTreasury({ btc: null, xmr: null, blurt: { base: Infinity } }));
if (r.ok || r.reason !== 'treasury_blurt_base_invalid') {
throw new Error(`expected treasury_blurt_base_invalid, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('cp372: blurt base over sanity ceiling → reject', () => {
const r = validateReleasePayload(withTreasury({ btc: null, xmr: null, blurt: { base: 10_000_001 } }));
if (r.ok || r.reason !== 'treasury_blurt_base_too_large') {
throw new Error(`expected treasury_blurt_base_too_large, got ${r.ok ? 'ok' : r.reason}`);
}
});
scenario('cp372: all three assets pinned together → ok', () => {
const r = validateReleasePayload(withTreasury({
btc: { address: VALID_BTC_ADDR, satoshis: 416 },
xmr: { address: VALID_XMR_ADDR, piconero: '781250000' },
blurt: { base: 62.5 }
}));
if (!r.ok) throw new Error('expected ok with all three');
if (r.value.treasury?.blurt?.base !== 62.5) throw new Error('blurt base lost');
if (r.value.treasury?.btc?.satoshis !== 416) throw new Error('btc lost');
});
// ─── cp556 — decentralized-distribution anchor ──────────────────────
const D_SHA = 'a'.repeat(64);
const D_FPR = 'DEADBEEF'.repeat(5); // 40-hex v4 fingerprint
const D_CID0 = 'Qm' + 'a'.repeat(44); // CIDv0 base58btc
const D_CID1 = 'b' + 'a'.repeat(58); // CIDv1 base32
function withDistribution(distribution: unknown): unknown {
return makePayload({ distribution });
}
scenario('distribution undefined → ok (back-compat)', () => {
const r = validateReleasePayload(makePayload());
if (!r.ok) throw new Error(`got ${r.reason}`);
if ('distribution' in r.value) throw new Error('phantom distribution key');
});
scenario('distribution=null → ok, omitted from value', () => {
const r = validateReleasePayload(withDistribution(null));
if (!r.ok) throw new Error(`got ${r.reason}`);
if ('distribution' in r.value) throw new Error('null distribution should be omitted');
});
scenario('distribution minimal (sha + fpr) → ok', () => {
const r = validateReleasePayload(withDistribution({ source_sha256: D_SHA, gpg_fingerprint: D_FPR }));
if (!r.ok) throw new Error(`got ${r.reason}`);
if (r.value.distribution?.source_sha256 !== D_SHA) throw new Error('sha lost');
if (r.value.distribution?.gpg_fingerprint !== D_FPR) throw new Error('fpr lost');
if ('ipfs_cid' in (r.value.distribution ?? {})) throw new Error('phantom ipfs_cid');
});
scenario('distribution full (cid v0 + mirrors) → ok', () => {
const r = validateReleasePayload(
withDistribution({
source_sha256: D_SHA,
gpg_fingerprint: D_FPR,
ipfs_cid: D_CID0,
mirrors: ['https://codeberg.org/agorise/morphit', 'https://ipfs.io/ipfs/' + D_CID0]
})
);
if (!r.ok) throw new Error(`got ${r.reason}`);
if (r.value.distribution?.ipfs_cid !== D_CID0) throw new Error('cid lost');
if (r.value.distribution?.mirrors?.length !== 2) throw new Error('mirrors lost');
});
scenario('distribution CIDv1 base32 → ok', () => {
const r = validateReleasePayload(
withDistribution({ source_sha256: D_SHA, gpg_fingerprint: D_FPR, ipfs_cid: D_CID1 })
);
if (!r.ok) throw new Error(`got ${r.reason}`);
});
scenario('distribution 64-hex (v5) fingerprint → ok', () => {
const r = validateReleasePayload(
withDistribution({ source_sha256: D_SHA, gpg_fingerprint: 'abcdef01'.repeat(8) })
);
if (!r.ok) throw new Error(`got ${r.reason}`);
});
scenario('distribution as array → distribution_not_object', () => {
const r = validateReleasePayload(withDistribution([]));
if (r.ok || r.reason !== 'distribution_not_object') throw new Error(`got ${r.ok ? 'ok' : r.reason}`);
});
scenario('distribution missing sha → distribution_source_sha256_invalid', () => {
const r = validateReleasePayload(withDistribution({ gpg_fingerprint: D_FPR }));
if (r.ok || r.reason !== 'distribution_source_sha256_invalid')
throw new Error(`got ${r.ok ? 'ok' : r.reason}`);
});
scenario('distribution UPPERCASE sha → distribution_source_sha256_invalid', () => {
const r = validateReleasePayload(withDistribution({ source_sha256: 'A'.repeat(64), gpg_fingerprint: D_FPR }));
if (r.ok || r.reason !== 'distribution_source_sha256_invalid')
throw new Error(`got ${r.ok ? 'ok' : r.reason}`);
});
scenario('distribution short fingerprint → distribution_gpg_fingerprint_invalid', () => {
const r = validateReleasePayload(withDistribution({ source_sha256: D_SHA, gpg_fingerprint: 'a'.repeat(39) }));
if (r.ok || r.reason !== 'distribution_gpg_fingerprint_invalid')
throw new Error(`got ${r.ok ? 'ok' : r.reason}`);
});
scenario('distribution bad cid → distribution_ipfs_cid_invalid', () => {
const r = validateReleasePayload(
withDistribution({ source_sha256: D_SHA, gpg_fingerprint: D_FPR, ipfs_cid: 'nope' })
);
if (r.ok || r.reason !== 'distribution_ipfs_cid_invalid')
throw new Error(`got ${r.ok ? 'ok' : r.reason}`);
});
scenario('distribution mirrors not array → distribution_mirrors_not_array', () => {
const r = validateReleasePayload(
withDistribution({ source_sha256: D_SHA, gpg_fingerprint: D_FPR, mirrors: 'https://x.example.org' })
);
if (r.ok || r.reason !== 'distribution_mirrors_not_array')
throw new Error(`got ${r.ok ? 'ok' : r.reason}`);
});
scenario('distribution non-https mirror → distribution_mirror_invalid', () => {
const r = validateReleasePayload(
withDistribution({ source_sha256: D_SHA, gpg_fingerprint: D_FPR, mirrors: ['http://x.example.org'] })
);
if (r.ok || r.reason !== 'distribution_mirror_invalid')
throw new Error(`got ${r.ok ? 'ok' : r.reason}`);
});
// v1.9.6 (Ken) — the mirror cap was bumped 8 → 10 (to fit the gitea.com +
// framagit.org additions). Pin the NEW boundary: 10 is accepted (at the cap),
// 11 is rejected (over it). A forward-compat note lives on MIRRORS_MAX.
scenario('distribution 10 mirrors (at the cap) → ok', () => {
const r = validateReleasePayload(
withDistribution({
source_sha256: D_SHA,
gpg_fingerprint: D_FPR,
mirrors: Array.from({ length: 10 }, (_, i) => `https://m${i}.example.org/x`)
})
);
if (!r.ok) throw new Error(`got ${r.reason}`);
if (r.value.distribution?.mirrors?.length !== 10) throw new Error('mirrors lost');
});
scenario('distribution 11 mirrors (over the cap) → distribution_mirror_invalid', () => {
const r = validateReleasePayload(
withDistribution({
source_sha256: D_SHA,
gpg_fingerprint: D_FPR,
mirrors: Array.from({ length: 11 }, (_, i) => `https://m${i}.example.org/x`)
})
);
if (r.ok || r.reason !== 'distribution_mirror_invalid')
throw new Error(`got ${r.ok ? 'ok' : r.reason}`);
});
// v1.8.16 (Ken) — Launchpad personal-repo git URLs carry a `+` (`/+git/`); the
// mirror charset was relaxed to allow it. Pin BOTH directions: the `+` URL is
// accepted, and a genuinely-bad char (space) is STILL rejected, so the relaxation
// didn't open the charset wide.
scenario('distribution mirror with + in path (Launchpad) → ok', () => {
const r = validateReleasePayload(
withDistribution({
source_sha256: D_SHA,
gpg_fingerprint: D_FPR,
mirrors: ['https://git.launchpad.net/~agorise/+git/morphit']
})
);
if (!r.ok) throw new Error(`got ${r.reason}`);
if (r.value.distribution?.mirrors?.[0] !== 'https://git.launchpad.net/~agorise/+git/morphit')
throw new Error('launchpad mirror lost');
});
scenario('distribution mirror with a space → distribution_mirror_invalid', () => {
const r = validateReleasePayload(
withDistribution({
source_sha256: D_SHA,
gpg_fingerprint: D_FPR,
mirrors: ['https://x.example.org/a b']
})
);
if (r.ok || r.reason !== 'distribution_mirror_invalid')
throw new Error(`got ${r.ok ? 'ok' : r.reason}`);
});
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);
}