deviant/scripts/chipnet-t001-verify.mjs
walerikus 599202a721 chore: checkpoint working tree — shell Tier-B retirement + accumulated WIP
Snapshot of accumulated work-in-progress on top of the baseline commit.

This session — retire Tier B from the wallet shell (single-tier model, glossary
"Tiers & trust"):
- SendFlow.tsx: BCH-only; removed the non-functional Tier-B send path, the D-4/D-5
  raw-BCH co-display, and the ADR-0013 friction ladder.
- ArtemisionScreen.tsx: Create wizard collapsed to a single 5-step on-chain genesis
  flow; tier-selection step + Tier-B branch (prepareTierBImport) deleted.
- QuickActions.tsx / SettingsScreen.tsx: receive note reworded; "Send friction
  thresholds" + "Tier explainer" rows removed; registry-trust note reworded.
- Verified: tsc clean, vitest 74/74, full preview walkthrough, no console errors.

Pre-existing uncommitted work also folded in:
- USDm -> FIATm family rename/reorg (Parameters/FIATm/, design records)
- ~130 chipnet + sim-fiatm scripts (catalytic-converter, hyperinflation backtests)
- snapshots 0153-0168; wiki updates (glossary, decisions, index); ADR-0044/0045
- mockups + additional shell screens (Agora, Apps, MainScreen, AssetDetail, ...)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 01:01:37 +02:00

223 lines
9.3 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-t001-verify.mjs — victim-side defenses against the T-001 rug.
*
* Step (d) of the T-001 PoC (snapshot 0018 "Next" item 3). Runs the two
* wallet-side checks from snapshot 0018 against the on-chain state left
* by chipnet-t001-attack.mjs and confirms each one fires.
*
* D-1 — Canonical contract bytecode pinning.
* The wallet ships the canonical DeviantOpenMintFT template.
* For the contract anchoring the RUG category it reconstructs
* what the canonical contract address WOULD be for the published
* params, and compares to the contract address actually holding
* the reservoir. The backdoor contract has extra bytecode (the
* founderFreeMint path + founderPk param) → a different P2SH32
* address → the comparison fails → token flagged "non-standard,
* peg not guaranteed".
*
* D-2 — Per-UTXO backing check at receive time.
* For every incoming RUG FT UTXO the wallet checks
* utxo.value >= ft_amount * satsPerUnit. The underbacked UTXO
* from the rug carries ~675 sats against a multi-million-sat
* peg requirement → flagged UNDERBACKED, excluded from the
* redeemable peg balance.
*
* IMPORTANT — this is PoC-grade verification logic living in the test
* script on purpose. The production D-1/D-2 implementation belongs in a
* dedicated src/verify/ module folded into the UTXO classification
* engine (snapshot 0018 "Next" items 1-2); that is separate scoped work.
* This script proves the checks are sound and catch the live attack.
*
* Env vars:
* VICTIM=a|b|c whose wallet runs the checks (default c — must match
* the VICTIM used in chipnet-t001-attack.mjs)
*
* Exit code 0 = both defenses fired (T-001 neutralised).
* Exit code 1 = a defense failed to fire, or setup is incomplete.
*/
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { TestNetWallet } from 'mainnet-js';
import { Contract, ElectrumNetworkProvider } from 'cashscript';
const ROOT = join(import.meta.dirname, '..');
const T001_FILE = join(ROOT, 'ChipnetTests', '.chipnet-t001-token.json');
const CANONICAL_ARTIFACT_PATH = join(ROOT, 'contracts/dist/DeviantOpenMintFT.json');
const BACKDOOR_ARTIFACT_PATH = join(ROOT, 'contracts/dist/DeviantOpenMintFT_Backdoor.json');
const VICTIM = (process.env.VICTIM ?? 'c').toLowerCase();
const KEYS = {
a: join(ROOT, '.chipnet-keys.json'),
b: join(ROOT, '.chipnet-keys-b.json'),
c: join(ROOT, '.chipnet-keys-c.json'),
};
if (!KEYS[VICTIM]) throw new Error(`VICTIM must be a/b/c, got ${VICTIM}`);
function hexToBin(hex) {
if (hex.length % 2 !== 0) throw new Error(`odd hex length: ${hex.length}`);
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
return out;
}
function txidToConsensusBytes(txidHex) {
const display = hexToBin(txidHex);
const out = new Uint8Array(display.length);
for (let i = 0; i < display.length; i++) out[i] = display[display.length - 1 - i];
return out;
}
function divider() { console.log('='.repeat(72)); }
async function main() {
const t001 = JSON.parse(readFileSync(T001_FILE, 'utf8'));
const canonicalArtifact = JSON.parse(readFileSync(CANONICAL_ARTIFACT_PATH, 'utf8'));
const backdoorArtifact = JSON.parse(readFileSync(BACKDOOR_ARTIFACT_PATH, 'utf8'));
const { wif } = JSON.parse(readFileSync(KEYS[VICTIM], 'utf8'));
const victim = await TestNetWallet.fromWIF(wif);
const provider = new ElectrumNetworkProvider('chipnet');
const rateSats = BigInt(t001.rateSats);
const tokenCategory = txidToConsensusBytes(t001.categoryId);
const founderLockingBytecode = hexToBin(t001.founderLockingBytecodeHex);
divider();
console.log(`T-001 victim-side defenses — Wallet ${VICTIM.toUpperCase()} (${victim.cashaddr})`);
console.log(` Token under test: ${t001.name} (category ${t001.categoryId.slice(0, 16)}...)`);
console.log(` Published rate: ${rateSats} sats/unit`);
divider();
let d1Fired = false;
let d2Fired = false;
// ---- D-1: canonical contract bytecode pinning -------------------------
console.log('');
console.log('D-1 — Canonical contract bytecode pinning');
console.log('-'.repeat(72));
// The wallet's embedded canonical template, instantiated with the
// token's PUBLISHED params. If the token were honest, this address
// would equal the contract that actually holds the reservoir.
const canonicalContract = new Contract(
canonicalArtifact,
[tokenCategory, rateSats, BigInt(t001.founderFeeBps), founderLockingBytecode],
{ provider },
);
// Confirm the contract address from metadata actually holds the
// minting-NFT reservoir for the claimed category (so we are checking
// the right contract, not a decoy).
const contractUtxos = await provider.getUtxos(t001.contractTokenAddress);
const reservoir = contractUtxos.find(
(u) => u.token?.nft?.capability === 'minting' && u.token?.category === t001.categoryId,
);
if (!reservoir) {
console.log(` FAIL: no minting-NFT reservoir for category at ${t001.contractTokenAddress}`);
console.log(' Has chipnet:t001-genesis run and confirmed?');
process.exit(1);
}
console.log(` On-chain contract (holds reservoir): ${t001.contractTokenAddress}`);
console.log(` Canonical template @ published params: ${canonicalContract.tokenAddress}`);
if (canonicalContract.tokenAddress === t001.contractTokenAddress) {
console.log(' D-1 result: contract IS canonical — D-1 did not flag it.');
} else {
d1Fired = true;
console.log(' D-1 result: MISMATCH — on-chain contract is NOT the canonical');
console.log(' DeviantOpenMintFT template. Token flagged');
console.log(' "non-standard CashToken — peg not guaranteed".');
console.log(' → no protocol-swap eligibility, no peg display.');
// Corroborate: show the on-chain address DOES match the backdoor
// template — i.e. we have positively identified the rug contract.
const backdoorContract = new Contract(
backdoorArtifact,
[
tokenCategory,
rateSats,
BigInt(t001.founderFeeBps),
founderLockingBytecode,
hexToBin(t001.founderPkHex),
],
{ provider },
);
const matchesBackdoor = backdoorContract.tokenAddress === t001.contractTokenAddress;
console.log(` Corroboration: on-chain address ${matchesBackdoor ? 'matches' : 'does NOT match'} ` +
`the known DeviantOpenMintFT_Backdoor template.`);
}
// ---- D-2: per-UTXO backing check at receive time ----------------------
console.log('');
console.log('D-2 — Per-UTXO backing check at receive time');
console.log('-'.repeat(72));
const victimUtxos = await provider.getUtxos(victim.cashaddr);
const rugFtUtxos = victimUtxos.filter(
(u) => u.token?.category === t001.categoryId && !u.token?.nft && u.token?.amount > 0n,
);
if (rugFtUtxos.length === 0) {
console.log(` No ${t001.name} FT UTXOs in Wallet ${VICTIM.toUpperCase()}.`);
console.log(' Has chipnet:t001-attack run and confirmed?');
process.exit(1);
}
let redeemableTotal = 0n;
let pegRequiredTotal = 0n;
let underbackedCount = 0;
for (const u of rugFtUtxos) {
const ftAmount = u.token.amount;
const value = BigInt(u.satoshis);
const expected = ftAmount * rateSats;
redeemableTotal += value;
pegRequiredTotal += expected;
const underbacked = value < expected;
if (underbacked) underbackedCount += 1;
console.log(
` UTXO ${u.txid.slice(0, 16)}:${u.vout} — ${ftAmount} ${t001.name}, ` +
`${value} sats`,
);
console.log(
` peg requires ${expected} sats → ${underbacked ? 'UNDERBACKED' : 'ok'}` +
(underbacked ? ` (carries ${(value * 10000n) / expected} bps of peg)` : ''),
);
}
if (underbackedCount > 0) {
d2Fired = true;
console.log('');
console.log(` D-2 result: ${underbackedCount} underbacked UTXO(s) flagged.`);
console.log(` Redeemable now: ${redeemableTotal} sats`);
console.log(` Peg implies: ${pegRequiredTotal} sats`);
console.log(` Shortfall: ${pegRequiredTotal - redeemableTotal} sats`);
console.log(' Underbacked UTXOs excluded from redeemable peg balance.');
} else {
console.log(' D-2 result: all UTXOs fully backed — D-2 did not flag anything.');
}
// ---- Verdict ----------------------------------------------------------
console.log('');
divider();
if (d1Fired && d2Fired) {
console.log('T-001 NEUTRALISED — both defenses fired.');
console.log('The wallet correctly refuses to treat RUG as Deviant-compliant:');
console.log(' D-1 caught the non-canonical contract;');
console.log(' D-2 caught the underbacked UTXO independently.');
divider();
process.exit(0);
} else {
console.log('T-001 NOT fully neutralised:');
console.log(` D-1 ${d1Fired ? 'fired' : 'did NOT fire'}`);
console.log(` D-2 ${d2Fired ? 'fired' : 'did NOT fire'}`);
console.log('Expected both to fire against the rug. Investigate before relying on these checks.');
divider();
process.exit(1);
}
}
main().catch((err) => {
console.error('FAILED:', err?.message ?? err);
if (err?.stack) console.error(err.stack);
process.exit(1);
});