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>
353 lines
22 KiB
JavaScript
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);});
|