deviant/scripts/chipnet-phase-f-test.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

353 lines
22 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-phase-f-test.mjs — Phase F of the voting-systems §6 design.
* Elder tier (rank 0x03) + ElderFreezeAction (Layer 4 of §6.7.1 founder-
* capture mitigation: Elder quorum freezes a pending founder action) +
* ComplaintCovenant (§6.10 rising-cost anti-complaint-spam).
*
* Phase F MVP scope: minimal viable Elder + freeze + complaint mechanics.
* Strict 3/3 Elder unanimous is parameterised (constructor freezeQuorum)
* to 2 for the chipnet demo so the existing 4-wallet test fixture can
* exercise the flow without provisioning more wallets. Production
* configures freezeQuorum=3 for constitutional actions.
*
* Tests:
* ELDER-1 — mint rank-0x03 Elder tokens to A and B
*
* FREEZE PATH:
* 1. Founder C creates a "pending action" UTXO (ElderFreezeAction
* seeded with 10000 sat escrow at the action address)
* 2. Elders A + B (freezeQuorum=2) co-spend their Elder tokens +
* the action UTXO → action FROZEN, escrow flows to closer
*
* EXECUTE PATH:
* 3. Founder C creates a fresh pending action (10000 sats)
* 4. Founder C executes — sig-checked; UTXO consumed; escrow claimed
*
* NEGATIVE — non-Elder rank tries to freeze (must FAIL):
* 5. Fresh pending action
* 6. Try freeze with a CITIZEN rank (0x01) token at input 1
* — covenant MUST REJECT on the rank gate
*
* COMPLAINT PATH:
* 7. Seed ComplaintCovenant against target B (baseFee=1000,
* elderEscalationThreshold=3 — so 3 complaints reach Elder
* escalation; demonstrates ramp 1000 → 2000 → 4000)
* 8. A complains (oldCount 0 → fee=1000)
* 9. D complains (oldCount 1 → fee=2000)
* 10. drain() blocked until elderEscalationThreshold reached
* 11. A complains (oldCount 2 → fee=4000); now count=3 = threshold
* 12. drain() succeeds; accumulated escrow flows to caller
*
* NEGATIVE — complain past threshold (must FAIL):
* 13. Try a 4th complaint against B (oldCount=3 ≥ threshold) — rejected
*/
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { TestNetWallet } from 'mainnet-js';
import { Contract, ElectrumNetworkProvider, TransactionBuilder, SignatureTemplate } from 'cashscript';
import { secp256k1 } from '@bitauth/libauth';
const ROOT = join(import.meta.dirname, '..');
const STATE = join(ROOT, 'ChipnetTests', '.chipnet-phase-f-test.json');
const CIT_ART = join(ROOT, 'contracts/dist/CitizenToken.json');
const EFA_ART = join(ROOT, 'contracts/dist/ElderFreezeAction.json');
const CMP_ART = join(ROOT, 'contracts/dist/ComplaintCovenant.json');
const KEYS = {
a: join(ROOT, '.chipnet-keys.json'),
b: join(ROOT, '.chipnet-keys-b.json'),
c: join(ROOT, '.chipnet-keys-c.json'),
d: join(ROOT, '.chipnet-keys-d.json'),
};
const CIT_DUST = 1000n;
const ACTION_DUST = 10000n; // seed value for pending action escrow
const CMP_SEED = 1000n;
const SEED_DUST = 1000n;
const MF = 2500n;
const RANK_CITIZEN = '01';
const RANK_DELEGATED = '02';
const RANK_ELDER = '03';
const FREEZE_QUORUM = 2n; // 2 Elders required for freeze (test scale)
const BASE_FEE = 1000n; // complaint baseFee
const ESC_THRESHOLD = 3n; // 3 complaints to Elder escalation
function hexToBin(h){const o=new Uint8Array(h.length/2);for(let i=0;i<o.length;i++)o[i]=parseInt(h.slice(i*2,i*2+2),16);return o;}
function binToHex(b){return Array.from(b,(x)=>x.toString(16).padStart(2,'0')).join('');}
function txidLE(h){const d=hexToBin(h);const o=new Uint8Array(d.length);for(let i=0;i<d.length;i++)o[i]=d[d.length-1-i];return o;}
function u64le(n){let v=BigInt(n);const o=new Uint8Array(8);for(let i=0;i<8;i++){o[i]=Number(v&0xffn);v>>=8n;}return binToHex(o);}
function leU64(hex){return BigInt('0x'+hex.match(/../g).reverse().join(''));}
const sleep=(ms)=>new Promise(r=>setTimeout(r,ms));
function divider(c='='){console.log(c.repeat(80));}
function step(t){console.log('');divider();console.log(t);divider();}
function load(){return existsSync(STATE)?JSON.parse(readFileSync(STATE,'utf8')):{};}
function save(s){writeFileSync(STATE,JSON.stringify(s,(k,v)=>typeof v==='bigint'?v.toString():v,2));}
function pickBch(u,need){const p=[];let t=0n;for(const x of u){p.push(x);t+=BigInt(x.satoshis);if(t>=need)break;}return{p,t};}
function txidOf(s){return typeof s==='string'?s:s?.txid??s?.txId;}
async function main(){
const citArt=JSON.parse(readFileSync(CIT_ART,'utf8'));
const efaArt=JSON.parse(readFileSync(EFA_ART,'utf8'));
const cmpArt=JSON.parse(readFileSync(CMP_ART,'utf8'));
for(const [k,f] of Object.entries(KEYS)) if(!existsSync(f)) throw new Error(`missing key ${k}`);
const A=await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.a,'utf8')).wif);
const B=await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.b,'utf8')).wif);
const C=await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.c,'utf8')).wif);
const D=await TestNetWallet.fromWIF(JSON.parse(readFileSync(KEYS.d,'utf8')).wif);
const provider=new ElectrumNetworkProvider('chipnet');
const sig={a:new SignatureTemplate(A.privateKey),b:new SignatureTemplate(B.privateKey),c:new SignatureTemplate(C.privateKey),d:new SignatureTemplate(D.privateKey)};
const cPub={a:secp256k1.derivePublicKeyCompressed(A.privateKey),b:secp256k1.derivePublicKeyCompressed(B.privateKey),c:secp256k1.derivePublicKeyCompressed(C.privateKey),d:secp256k1.derivePublicKeyCompressed(D.privateKey)};
let st=load();
async function eligible(w){
let u=await w.getUtxos();let e=u.filter(x=>x.vout===0&&!x.token);
if(e.length)return e[0];
const bad=u.filter(x=>!(x.vout===0&&!x.token));
const r=await w.send([{cashaddr:w.cashaddr,value:2000n,unit:'sat'}],bad.length?{utxoIds:bad}:undefined);
console.log(` splitter ${txidOf(r)}`);await sleep(12000);
u=await w.getUtxos();e=u.filter(x=>x.vout===0&&!x.token);if(!e.length)throw new Error('splitter failed');return e[0];
}
async function pickC(need){const u=(await provider.getUtxos(C.cashaddr)).filter(x=>!x.token).sort((a,b)=>Number(b.satoshis)-Number(a.satoshis));const {p,t}=pickBch(u,need);if(t<need)throw new Error(`C low BCH ${t}<${need}`);return {p,t};}
async function pickW(w,need){const u=(await provider.getUtxos(w.cashaddr)).filter(x=>!x.token).sort((a,b)=>Number(b.satoshis)-Number(a.satoshis));const {p,t}=pickBch(u,need);if(t<need)throw new Error(`low BCH ${t}<${need}`);return {p,t};}
// ---- Genesis citizen NFT category + action category + complaint category ----
step('Genesis citizen NFT category');
if(!st.citCat){const u=await eligible(C);const cat=u.txid;const r=await C.tokenGenesis({cashaddr:C.tokenaddr,amount:0n,value:SEED_DUST,nft:{capability:'minting',commitment:''}},[],{utxoIds:[u]});console.log(` citizen genesis ${txidOf(r)} — ${cat}`);st.citCat=cat;save(st);await sleep(10000);} else console.log(` citCat ${st.citCat}`);
const citCat=st.citCat;
step('Genesis complaint NFT category');
if(!st.cmpCat){const u=await eligible(C);const cat=u.txid;const r=await C.tokenGenesis({cashaddr:C.tokenaddr,amount:0n,value:SEED_DUST,nft:{capability:'minting',commitment:''}},[],{utxoIds:[u]});console.log(` complaint genesis ${txidOf(r)} — ${cat}`);st.cmpCat=cat;save(st);await sleep(10000);} else console.log(` cmpCat ${st.cmpCat}`);
const cmpCat=st.cmpCat;
// Per-holder CitizenToken contracts
const citA=new Contract(citArt,[A.publicKeyHash],{provider});
const citB=new Contract(citArt,[B.publicKeyHash],{provider});
const citD=new Contract(citArt,[D.publicKeyHash],{provider});
console.log(` CitizenToken(A) ${citA.tokenAddress}`);
console.log(` CitizenToken(B) ${citB.tokenAddress}`);
console.log(` CitizenToken(D) ${citD.tokenAddress}`);
// ElderFreezeAction — single instance for the test
const efa=new Contract(efaArt,[C.publicKeyHash,txidLE(citCat),FREEZE_QUORUM],{provider});
console.log(` ElderFreezeAction ${efa.tokenAddress}`);
// ComplaintCovenant against B
const cmpB=new Contract(cmpArt,[B.publicKeyHash,txidLE(cmpCat),txidLE(citCat),BASE_FEE,ESC_THRESHOLD],{provider});
console.log(` ComplaintCovenant(B) ${cmpB.tokenAddress}`);
async function mintingUtxo(category){return (await provider.getUtxos(C.cashaddr)).find(x=>x.token?.category===category&&x.token?.nft?.capability==='minting');}
async function tokenAt(addr,category,commitment){return (await provider.getUtxos(addr)).find(x=>x.token?.category===category&&x.token?.nft?.capability==='mutable'&&(commitment===undefined||x.token.nft.commitment===commitment));}
async function anyMutAt(addr,category){return (await provider.getUtxos(addr)).find(x=>x.token?.category===category&&x.token?.nft?.capability==='mutable');}
async function mintCitizen(targetContract,label,rank){
const key=`mint${label}r${rank}`;
if(st[key])return;
const existing=await tokenAt(targetContract.tokenAddress,citCat,rank);
if(existing){console.log(` (${label} already holds rank ${rank})`);st[key]='already-present';save(st);return;}
const m=await mintingUtxo(citCat);if(!m)throw new Error(`mint ${label}: citizen minting NFT missing at C`);
const {p,t}=await pickC(CIT_DUST+MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(m,sig.c.unlockP2PKH());
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
tb.addOutput({to:C.tokenaddr,amount:SEED_DUST,token:{amount:0n,category:citCat,nft:{capability:'minting',commitment:''}}});
tb.addOutput({to:targetContract.tokenAddress,amount:CIT_DUST,token:{amount:0n,category:citCat,nft:{capability:'mutable',commitment:rank}}});
tb.addOutput({to:C.cashaddr,amount:BigInt(m.satoshis)+t-SEED_DUST-CIT_DUST-MF});
st[key]=txidOf(await tb.send());save(st);console.log(` minted rank ${rank} CitizenToken to ${label} — ${st[key]}`);await sleep(10000);
}
step('Mint Elder tokens (rank 0x03) to A and B; Citizen (0x01) to D for negative probe');
await mintCitizen(citA,'A',RANK_ELDER);
await mintCitizen(citB,'B',RANK_ELDER);
await mintCitizen(citD,'D',RANK_CITIZEN);
// ---- Seed an ElderFreezeAction UTXO with 10000 sat escrow ----
async function seedAction(labelKey){
if(st[labelKey])return;
const {p,t}=await pickC(ACTION_DUST+MF+1000n);
const tb=new TransactionBuilder({provider});
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
tb.addOutput({to:efa.tokenAddress,amount:ACTION_DUST}); // no token; just BCH escrow
tb.addOutput({to:C.cashaddr,amount:t-ACTION_DUST-MF});
st[labelKey]=txidOf(await tb.send());save(st);console.log(` ${labelKey} action seeded with ${ACTION_DUST} sats — ${st[labelKey]}`);await sleep(10000);
}
async function actionUtxo(){const us=await provider.getUtxos(efa.tokenAddress);return us.find(x=>!x.token);}
// ===== FREEZE PATH =====
step('FREEZE PATH — seed action');
await seedAction('action1Seed');
step('FREEZE PATH — Elders A + B freeze the action (quorum=2)');
if(!st.action1Freeze){
const a=await actionUtxo();if(!a)throw new Error('action1Freeze: action UTXO missing');
const eA=await tokenAt(citA.tokenAddress,citCat,RANK_ELDER);
const eB=await tokenAt(citB.tokenAddress,citCat,RANK_ELDER);
if(!eA||!eB)throw new Error('action1Freeze: Elder tokens missing');
const {p,t}=await pickW(A,MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(a,efa.unlock.freeze()); // [0] action
tb.addInput(eA,citA.unlock.vote(cPub.a,sig.a)); // [1] Elder A
tb.addInput(eB,citB.unlock.vote(cPub.b,sig.b)); // [2] Elder B
for(const x of p)tb.addInput(x,sig.a.unlockP2PKH());
// Output ordering matches input ordering for CitizenToken.vote() preservation:
// [0] A's payout (action has no output constraint — freeze() just consumes)
// [1] Elder A's CitizenToken preserved (matches input 1)
// [2] Elder B's CitizenToken preserved (matches input 2)
tb.addOutput({to:A.cashaddr,amount:BigInt(a.satoshis)+t-MF});
tb.addOutput({to:citA.tokenAddress,amount:CIT_DUST,token:{amount:0n,category:citCat,nft:{capability:'mutable',commitment:RANK_ELDER}}});
tb.addOutput({to:citB.tokenAddress,amount:CIT_DUST,token:{amount:0n,category:citCat,nft:{capability:'mutable',commitment:RANK_ELDER}}});
st.action1Freeze=txidOf(await tb.send());save(st);console.log(` FROZEN — A claimed ${a.satoshis} sat escrow — ${st.action1Freeze}`);await sleep(10000);
} else console.log(` (frozen ${st.action1Freeze})`);
// ===== EXECUTE PATH =====
step('EXECUTE PATH — seed fresh action');
await seedAction('action2Seed');
step('EXECUTE PATH — founder C executes the action (sig-checked)');
if(!st.action2Execute){
const a=await actionUtxo();if(!a)throw new Error('action2Execute: action UTXO missing');
const {p,t}=await pickC(MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(a,efa.unlock.execute(cPub.c,sig.c));
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
tb.addOutput({to:C.cashaddr,amount:BigInt(a.satoshis)+t-MF});
st.action2Execute=txidOf(await tb.send());save(st);console.log(` EXECUTED — C claimed ${a.satoshis} sat escrow — ${st.action2Execute}`);await sleep(10000);
} else console.log(` (executed ${st.action2Execute})`);
// ===== NEGATIVE PROBE: non-Elder rank tries to freeze =====
step('NEGATIVE — seed action, try freeze with Citizen-rank token (must FAIL)');
await seedAction('action3Seed');
let neg1Ok=st.neg1Probed===true;
if(!neg1Ok){
try {
const a=await actionUtxo();if(!a){console.log(` (action UTXO missing — skipping)`);neg1Ok=true;}
else {
const eA=await tokenAt(citA.tokenAddress,citCat,RANK_ELDER); // Elder at input 1 (OK)
const cD=await tokenAt(citD.tokenAddress,citCat,RANK_CITIZEN); // Citizen at input 2 (REJECT)
if(!eA||!cD)throw new Error('neg1: required tokens missing');
const {p,t}=await pickW(A,MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(a,efa.unlock.freeze());
tb.addInput(eA,citA.unlock.vote(cPub.a,sig.a));
tb.addInput(cD,citD.unlock.vote(cPub.d,sig.d)); // wrong rank!
for(const x of p)tb.addInput(x,sig.a.unlockP2PKH());
// Same ordering fix as positive path:
// [0] payout, [1] Elder A preserved, [2] Citizen D preserved
tb.addOutput({to:A.cashaddr,amount:BigInt(a.satoshis)+t-MF});
tb.addOutput({to:citA.tokenAddress,amount:CIT_DUST,token:{amount:0n,category:citCat,nft:{capability:'mutable',commitment:RANK_ELDER}}});
tb.addOutput({to:citD.tokenAddress,amount:CIT_DUST,token:{amount:0n,category:citCat,nft:{capability:'mutable',commitment:RANK_CITIZEN}}});
await tb.send();
console.log(` UNEXPECTED PASS — non-Elder freeze should have failed`);
}
} catch (e) {
console.log(` EXPECTED FAIL — non-Elder freeze rejected (${(e?.message||e).toString().slice(0,90)}...)`);
neg1Ok=true;
}
st.neg1Probed=neg1Ok;save(st);
} else console.log(` (neg1 already recorded as ${neg1Ok?'expected-fail':'unexpected-pass'})`);
// ===== COMPLAINT PATH =====
step('COMPLAINT — seed ComplaintCovenant against target B (count=0)');
if(!st.cmpSeed){
const m=await mintingUtxo(cmpCat);if(!m)throw new Error('cmpSeed: complaint minting NFT missing');
const {p,t}=await pickC(CMP_SEED+MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(m,sig.c.unlockP2PKH());
for(const x of p)tb.addInput(x,sig.c.unlockP2PKH());
tb.addOutput({to:C.tokenaddr,amount:SEED_DUST,token:{amount:0n,category:cmpCat,nft:{capability:'minting',commitment:''}}});
tb.addOutput({to:cmpB.tokenAddress,amount:CMP_SEED,token:{amount:0n,category:cmpCat,nft:{capability:'mutable',commitment:u64le(0n)}}});
tb.addOutput({to:C.cashaddr,amount:BigInt(m.satoshis)+t-SEED_DUST-CMP_SEED-MF});
st.cmpSeed=txidOf(await tb.send());save(st);console.log(` cmpSeed — ${st.cmpSeed}`);await sleep(10000);
} else console.log(` (seeded ${st.cmpSeed})`);
async function complain(wallet,signer,compPub,citizenContract,rankHex,labelKey){
if(st[labelKey])return;
const c=await anyMutAt(cmpB.tokenAddress,cmpCat);if(!c)throw new Error(`${labelKey}: cmp UTXO missing`);
const tok=await tokenAt(citizenContract.tokenAddress,citCat,rankHex);
if(!tok)throw new Error(`${labelKey}: CitizenToken (rank ${rankHex}) missing`);
const oldCount=leU64(c.token.nft.commitment);
const newCount=oldCount+1n;
// currentFee = baseFee · 2^oldCount
let mult=1n;
if(oldCount>=1n)mult=2n;
if(oldCount>=2n)mult=4n;
if(oldCount>=3n)mult=8n;
if(oldCount>=4n)mult=16n;
if(oldCount>=5n)mult=32n;
if(oldCount>=6n)mult=64n;
const currentFee=BASE_FEE*mult;
const newCommit=u64le(newCount);
const newValue=BigInt(c.satoshis)+currentFee;
const {p,t}=await pickW(wallet,currentFee+MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(c,cmpB.unlock.complain());
tb.addInput(tok,citizenContract.unlock.vote(compPub,signer));
for(const x of p)tb.addInput(x,signer.unlockP2PKH());
tb.addOutput({to:cmpB.tokenAddress,amount:newValue,token:{amount:0n,category:cmpCat,nft:{capability:'mutable',commitment:newCommit}}});
tb.addOutput({to:citizenContract.tokenAddress,amount:CIT_DUST,token:{amount:0n,category:citCat,nft:{capability:'mutable',commitment:rankHex}}});
tb.addOutput({to:wallet.cashaddr,amount:t-currentFee-MF});
st[labelKey]=txidOf(await tb.send());save(st);console.log(` ${labelKey} complaint count ${oldCount}->${newCount}, fee ${currentFee} sats, escrow ${c.satoshis}->${newValue} — ${st[labelKey]}`);await sleep(10000);
}
step('COMPLAINT — A complains (oldCount=0, fee=1000)');
await complain(A,sig.a,cPub.a,citA,RANK_ELDER,'cmpA1');
step('COMPLAINT — D complains (oldCount=1, fee=2000)');
await complain(D,sig.d,cPub.d,citD,RANK_CITIZEN,'cmpD1');
step('COMPLAINT — A complains again (oldCount=2, fee=4000); reaches threshold');
await complain(A,sig.a,cPub.a,citA,RANK_ELDER,'cmpA2');
step('DRAIN — count >= elderEscalationThreshold; A claims accumulated escrow');
if(!st.cmpDrain){
const c=await anyMutAt(cmpB.tokenAddress,cmpCat);if(!c)throw new Error('cmpDrain: cmp UTXO missing');
const {p,t}=await pickW(A,MF+1000n);
const tb=new TransactionBuilder({provider,allowImplicitFungibleTokenBurn:true});
tb.addInput(c,cmpB.unlock.drain());
for(const x of p)tb.addInput(x,sig.a.unlockP2PKH());
tb.addOutput({to:A.cashaddr,amount:BigInt(c.satoshis)+t-MF});
st.cmpDrain=txidOf(await tb.send());save(st);console.log(` drained — A claimed ${c.satoshis} sat accumulated escrow — ${st.cmpDrain}`);await sleep(10000);
} else console.log(` (drained ${st.cmpDrain})`);
// ===== VERIFY =====
step('VERIFY');
const eAToken=await tokenAt(citA.tokenAddress,citCat,RANK_ELDER);
const eBToken=await tokenAt(citB.tokenAddress,citCat,RANK_ELDER);
const cDToken=await tokenAt(citD.tokenAddress,citCat,RANK_CITIZEN);
const checks=[
['citizen + complaint categories genesised', !!citCat && !!cmpCat],
['rank-0x03 Elder tokens minted to A and B', !!eAToken && !!eBToken],
['rank-0x01 Citizen token minted to D (for negative probe)', !!cDToken],
['FREEZE — action 1 seeded with 10000 sat escrow', !!st.action1Seed],
['FREEZE — Elders A + B (quorum=2) consumed action; escrow → A', !!st.action1Freeze],
['EXECUTE — action 2 seeded fresh', !!st.action2Seed],
['EXECUTE — founder C executed; escrow → C', !!st.action2Execute],
['NEG — action 3 seeded', !!st.action3Seed],
['NEG — non-Elder freeze REJECTED by covenant', neg1Ok],
['COMPLAINT — ComplaintCovenant(B) seeded (count=0)', !!st.cmpSeed],
['COMPLAINT — A complained (count 0→1, fee 1000)', !!st.cmpA1],
['COMPLAINT — D complained (count 1→2, fee 2000)', !!st.cmpD1],
['COMPLAINT — A complained again (count 2→3 = threshold, fee 4000)', !!st.cmpA2],
['DRAIN — escrow claimed by A (count ≥ threshold)', !!st.cmpDrain],
];
let ok=true;for(const [l,p] of checks){console.log(` [${p?'PASS':'FAIL'}] ${l}`);ok=ok&&p;}
step('SUMMARY — Phase F: Elder tier + ElderFreezeAction + ComplaintCovenant');
console.log(` citCat ${citCat}\n cmpCat ${cmpCat}`);
console.log(` ElderFreezeAction ${efa.tokenAddress}`);
console.log(` ComplaintCovenant(B) ${cmpB.tokenAddress}`);
console.log(' ');
console.log(' Phase F validates three mechanisms:');
console.log(' 1. Elder tier — rank-0x03 CitizenToken (a new rank for constitutional roles).');
console.log(' 2. ElderFreezeAction — an Elder quorum (freezeQuorum=2 in test, =3 in prod)');
console.log(' co-spends Elder tokens to halt a pending founder action. The covenant');
console.log(' verifies each co-spent input carries rank 0x03 + the expected category.');
console.log(' Founder retains the execute() path via direct sig (Phase F MVP skips the');
console.log(' CLTV announcement delay; production adds it per §6.7.1 Layer 3).');
console.log(' 3. ComplaintCovenant — rising-cost-per-complaint (baseFee · 2^N) with Elder');
console.log(' escalation at threshold. Accumulated fees escrow in the covenant UTXO;');
console.log(' drain() claims them on Elder escalation (Phase F MVP: caller claims;');
console.log(' production routes to TreasuryVault.cash per addenda 4/5).');
console.log(' Phase F scope only — lazy-voter detection (audit-case insertion, pattern');
console.log(' analysis) is doc-only for Phase F; Phase G adds D-1 templates + wallet UI.');
divider();
if(!ok){console.error('VERIFY FAILED');process.exit(1);}
process.exit(0);
}
main().catch((e)=>{console.error('FAILED:',e?.message??e);if(e?.stack)console.error(e.stack);process.exit(1);});