deviant/scripts/chipnet-phase-e-test.mjs
walerikus 518a3ce174 chore: initial project baseline — full Deviant codebase under version control
Brings the whole project into git (previously only the on-ramp feature and
a few parallel files were tracked). Includes: wallet core (src/), CashScript
contracts/, the Capacitor shell (shells/), scripts/, tests, and the docs
corpus (wiki/, snapshots/, Parameters/, Archive/, KnowledgeBase text).

Excluded via .gitignore: node_modules/, dist/, .env (secrets), testnet WIF
key files (.chipnet-*.json, devWallets.ts), KnowledgeBase/RAW media (~9.6 GB),
transcripts/ (session logs), and machine-local config (.claude/*.local,
*.lock). Secret-scanned before commit — no credentials in the tree.

Note: src/swapMath.ts, src/swapBuilder.ts, contracts/OpenMintToken.cash are
legacy (OP_RETURN era) but still imported; committed as-is, flagged for a
separate removal once de-referenced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 20:38:07 +02:00

335 lines
21 KiB
JavaScript

#!/usr/bin/env node
/**
* chipnet-phase-e-test.mjs — Phase E of the voting-systems §6 design.
* §6.9 commit-reveal secret ballot: each vote is two transactions —
* COMMIT — voter binds (still-secret) vote by sha256 hash
* REVEAL — voter publishes preimage; covenant verifies; counter updates
*
* Tests two paths:
* APP1 (success path) — applicant D:
* 1. founder seeds D's app (escrow=20000)
* 2. founder mints two ballots (chamber 0 for A, chamber 1 for B) into
* their respective BallotCovenant addresses (commitment = 32 zeros)
* 3. A commits a YES vote (sets ballot commit = sha256(0x01||nonce||A_pkh))
* — Wait, sha256(bytes1(YES=0) ++ nonce ++ A_pkh). YES=0 in our encoding.
* 4. B commits a YES vote
* 5. A reveals — co-spends app + A's ballot; app counter rYes 0->1, A paid 4000
* 6. B reveals — co-spends app + B's ballot; app counter dYes 0->1, B paid 6000
* 7. close — A claims 10000 sat residual
*
* APP2 (NEGATIVE-PATH probe — wrong nonce at reveal must FAIL) — applicant B:
* 8. founder seeds B's app, mints A's ballot (chamber 0)
* 9. A commits with nonce N1
* 10. A tries to REVEAL with a DIFFERENT nonce N2 — covenant MUST REJECT
* on hash mismatch (BallotCovenant line ~57).
*
* Time gates (commit window / reveal window) are PRESENT in the contracts
* but NOT exercised in Phase E tests to keep wall-clock manageable.
* Phase F+ adds full CLTV-bracketed timing tests.
*/
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, sha256 } from '@bitauth/libauth';
import { randomBytes } from 'node:crypto';
const ROOT = join(import.meta.dirname, '..');
const STATE = join(ROOT, '.chipnet-phase-e-test.json');
const BAL_ART = join(ROOT, 'contracts/dist/BallotCovenant.json');
const APP_ART = join(ROOT, 'contracts/dist/ApplicationCovenantE.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 BAL_DUST = 1000n;
const SEED_DUST = 1000n;
const MF = 2500n;
const APP_OPEN_COMMIT = '0000'.repeat(8); // 16 bytes
const ZERO32 = '00'.repeat(32); // ballot starting commit
const REGULAR_THRESHOLD = 1n;
const DELEGATED_THRESHOLD = 1n;
const CITIZEN_REWARD = 4000n;
const DELEGATED_REWARD = 6000n;
const APP_INITIAL_VALUE = 2n * (CITIZEN_REWARD + DELEGATED_REWARD); // 20000
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 u16le(n){let v=BigInt(n);return binToHex(new Uint8Array([Number(v&0xffn),Number((v>>8n)&0xffn)]));}
const appCommit=(rY,rN,dY,dN,rA,dA,spareHex='00000000')=>u16le(rY)+u16le(rN)+u16le(dY)+u16le(dN)+u16le(rA)+u16le(dA)+spareHex;
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;}
function leU16(hex){return BigInt('0x'+hex.match(/../g).reverse().join(''));}
function decodeAppCommit(c){
return {rYes:leU16(c.slice(0,4)),rNo:leU16(c.slice(4,8)),dYes:leU16(c.slice(8,12)),dNo:leU16(c.slice(12,16)),rAtt:leU16(c.slice(16,20)),dAtt:leU16(c.slice(20,24)),spare:c.slice(24,32)};
}
// Build the 53-byte preimage and its sha256 hash, all hex.
function buildVoteHash(choice, nonceHex32, voterPkhHex20){
const preimage = new Uint8Array(1+32+20);
preimage[0] = choice;
preimage.set(hexToBin(nonceHex32), 1);
preimage.set(hexToBin(voterPkhHex20), 33);
return binToHex(sha256.hash(preimage));
}
async function main(){
const balArt=JSON.parse(readFileSync(BAL_ART,'utf8'));
const appArt=JSON.parse(readFileSync(APP_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)};
const pkhHex={a:binToHex(A.publicKeyHash),b:binToHex(B.publicKeyHash),c:binToHex(C.publicKeyHash),d:binToHex(D.publicKeyHash)};
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 3 categories: app-state, ballot ----
step('Genesis application-state NFT category');
if(!st.appCat){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(` app-state genesis ${txidOf(r)} — ${cat}`);st.appCat=cat;save(st);await sleep(10000);} else console.log(` appCat ${st.appCat}`);
const appCat=st.appCat;
step('Genesis ballot NFT category');
if(!st.balCat){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(` ballot genesis ${txidOf(r)} — ${cat}`);st.balCat=cat;save(st);await sleep(10000);} else console.log(` balCat ${st.balCat}`);
const balCat=st.balCat;
// BallotCovenant addresses (per-voter)
const balA=new Contract(balArt,[A.publicKeyHash,txidLE(balCat)],{provider});
const balB=new Contract(balArt,[B.publicKeyHash,txidLE(balCat)],{provider});
const balD=new Contract(balArt,[D.publicKeyHash,txidLE(balCat)],{provider});
console.log(` Ballot(A) ${balA.tokenAddress}`);
console.log(` Ballot(B) ${balB.tokenAddress}`);
console.log(` Ballot(D) ${balD.tokenAddress}`);
// ApplicationCovenantE: one per applicant
const appD=new Contract(appArt,[D.publicKeyHash,txidLE(appCat),txidLE(balCat),REGULAR_THRESHOLD,DELEGATED_THRESHOLD,CITIZEN_REWARD,DELEGATED_REWARD],{provider});
const appB=new Contract(appArt,[B.publicKeyHash,txidLE(appCat),txidLE(balCat),REGULAR_THRESHOLD,DELEGATED_THRESHOLD,CITIZEN_REWARD,DELEGATED_REWARD],{provider});
console.log(` AppE(D) ${appD.tokenAddress}`);
console.log(` AppE(B) ${appB.tokenAddress}`);
async function mintingUtxo(category){return (await provider.getUtxos(C.cashaddr)).find(x=>x.token?.category===category&&x.token?.nft?.capability==='minting');}
async function nftAt(addr,category,opts={}){
const ms=await provider.getUtxos(addr);
return ms.find(x=>x.token?.category===category && x.token?.nft?.capability==='mutable' && (opts.commitment===undefined || x.token.nft.commitment===opts.commitment));
}
async function anyNftAt(addr,category){
const ms=await provider.getUtxos(addr);
return ms.find(x=>x.token?.category===category && x.token?.nft?.capability==='mutable');
}
// ---- Mint a fresh (zero-commit) ballot to a voter's BallotCovenant address ----
async function mintBallot(balContract,label){
if(st[`balMint${label}`])return;
const m=await mintingUtxo(balCat);if(!m)throw new Error(`mint ${label}: ballot minting NFT missing at C`);
const {p,t}=await pickC(BAL_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:balCat,nft:{capability:'minting',commitment:''}}});
tb.addOutput({to:balContract.tokenAddress,amount:BAL_DUST,token:{amount:0n,category:balCat,nft:{capability:'mutable',commitment:ZERO32}}});
tb.addOutput({to:C.cashaddr,amount:BigInt(m.satoshis)+t-SEED_DUST-BAL_DUST-MF});
st[`balMint${label}`]=txidOf(await tb.send());save(st);console.log(` ballot minted to ${label} — ${st[`balMint${label}`]}`);await sleep(10000);
}
async function seedApp(app,labelKey){
if(st[labelKey])return;
const m=await mintingUtxo(appCat);if(!m)throw new Error(`${labelKey}: app-state minting NFT missing at C`);
const {p,t}=await pickC(APP_INITIAL_VALUE+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:appCat,nft:{capability:'minting',commitment:''}}});
tb.addOutput({to:app.tokenAddress,amount:APP_INITIAL_VALUE,token:{amount:0n,category:appCat,nft:{capability:'mutable',commitment:APP_OPEN_COMMIT}}});
tb.addOutput({to:C.cashaddr,amount:BigInt(m.satoshis)+t-SEED_DUST-APP_INITIAL_VALUE-MF});
st[labelKey]=txidOf(await tb.send());save(st);console.log(` ${labelKey} seeded — ${st[labelKey]}`);await sleep(10000);
}
// ---- COMMIT: voter sets their ballot's commitment to sha256(choice||nonce||pkh) ----
async function commitVote(balContract,wallet,signer,compPub,voterPkhHex,labelKey,choice,nonceHex){
if(st[labelKey])return;
const b=await nftAt(balContract.tokenAddress,balCat,{commitment:ZERO32});
if(!b)throw new Error(`${labelKey}: fresh ballot missing at ${balContract.tokenAddress}`);
const voteHash=buildVoteHash(choice,nonceHex,voterPkhHex);
const {p,t}=await pickW(wallet,MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(b,balContract.unlock.commit(hexToBin(voteHash),compPub,signer));
for(const x of p)tb.addInput(x,signer.unlockP2PKH());
tb.addOutput({to:balContract.tokenAddress,amount:BAL_DUST,token:{amount:0n,category:balCat,nft:{capability:'mutable',commitment:voteHash}}});
tb.addOutput({to:wallet.cashaddr,amount:t-MF});
st[labelKey]=txidOf(await tb.send());save(st);console.log(` ${labelKey} commit (hash=${voteHash.slice(0,16)}...) — ${st[labelKey]}`);await sleep(10000);
}
// ---- REVEAL: co-spend app + ballot; app reads choice from output[1] ----
async function revealVote(app,balContract,wallet,signer,compPub,labelKey,chamber,choice,nonceHex){
if(st[labelKey])return;
const a=await anyNftAt(app.tokenAddress,appCat);if(!a)throw new Error(`${labelKey}: app UTXO missing`);
// Find the BALLOT that holds the voter's voteHash (not the zero ballot)
const all=await provider.getUtxos(balContract.tokenAddress);
const b=all.find(x=>x.token?.category===balCat&&x.token?.nft?.capability==='mutable'&&x.token.nft.commitment!==ZERO32&&!x.token.nft.commitment.startsWith('00'.repeat(31))); // not zero, not already revealed
if(!b)throw new Error(`${labelKey}: committed ballot missing at ${balContract.tokenAddress}`);
const before=decodeAppCommit(a.token.nft.commitment);
let {rYes,rNo,dYes,dNo,rAtt,dAtt,spare}=before;
if(chamber===0){rAtt=rAtt+1n; if(choice===0)rYes=rYes+1n; else rNo=rNo+1n;}
else {dAtt=dAtt+1n; if(choice===0)dYes=dYes+1n; else dNo=dNo+1n;}
const newCommit=appCommit(rYes,rNo,dYes,dNo,rAtt,dAtt,spare);
const reward=chamber===0?CITIZEN_REWARD:DELEGATED_REWARD;
const appBefore=BigInt(a.satoshis);
const appAfter=appBefore-reward;
// Ballot's revealed commitment = bytes1(choice) + 31 zeros
const revealedBallotCommit=choice.toString(16).padStart(2,'0')+'00'.repeat(31);
const {p,t}=await pickW(wallet,MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(a,app.unlock.revealVote(BigInt(chamber)));
tb.addInput(b,balContract.unlock.reveal(BigInt(choice),hexToBin(nonceHex),compPub,signer));
for(const x of p)tb.addInput(x,signer.unlockP2PKH());
tb.addOutput({to:app.tokenAddress,amount:appAfter,token:{amount:0n,category:appCat,nft:{capability:'mutable',commitment:newCommit}}});
tb.addOutput({to:balContract.tokenAddress,amount:BAL_DUST,token:{amount:0n,category:balCat,nft:{capability:'mutable',commitment:revealedBallotCommit}}});
tb.addOutput({to:wallet.cashaddr,amount:t+reward-MF});
st[labelKey]=txidOf(await tb.send());save(st);
console.log(` ${labelKey} reveal ${choice===0?'YES':'NO'} in ${chamber===0?'regular':'delegated'} — app ${appBefore}→${appAfter}, voter +${reward} — ${st[labelKey]}`);
await sleep(10000);
}
// Persist nonces (test must remember them between commit & reveal)
if(!st.nonces){st.nonces={
aApp1: binToHex(randomBytes(32)),
bApp1: binToHex(randomBytes(32)),
aApp2: binToHex(randomBytes(32)),
aApp2WrongReveal: binToHex(randomBytes(32)), // different nonce for negative probe
};save(st);console.log(' generated fresh nonces');}
const nonces=st.nonces;
// ===== APP1 — success path =====
step('APP1 — seed D\'s application');
await seedApp(appD,'app1Seed');
step('APP1 — mint ballots to A (chamber 0) and B (chamber 1)');
await mintBallot(balA,'A1');
await mintBallot(balB,'B1');
step('APP1 — COMMIT phase: A & B each publish sha256(choice || nonce || pkh)');
await commitVote(balA,A,sig.a,cPub.a,pkhHex.a,'app1CommitA',0,nonces.aApp1); // A commits YES
await commitVote(balB,B,sig.b,cPub.b,pkhHex.b,'app1CommitB',0,nonces.bApp1); // B commits YES
step('APP1 — REVEAL phase: A & B reveal; app counter updates trustlessly');
await revealVote(appD,balA,A,sig.a,cPub.a,'app1RevealA',0,0,nonces.aApp1);
await revealVote(appD,balB,B,sig.b,cPub.b,'app1RevealB',1,0,nonces.bApp1);
step('APP1 — close()');
if(!st.app1Close){
const a=await anyNftAt(appD.tokenAddress,appCat);if(!a)throw new Error('app1Close: missing');
const residual=BigInt(a.satoshis);
const {p,t}=await pickW(A,MF+1000n);
const tb=new TransactionBuilder({provider,allowImplicitFungibleTokenBurn:true});
tb.addInput(a,appD.unlock.close());
for(const x of p)tb.addInput(x,sig.a.unlockP2PKH());
tb.addOutput({to:A.cashaddr,amount:residual+t-MF});
st.app1Close=txidOf(await tb.send());save(st);console.log(` closed — A claimed ${residual} sat — ${st.app1Close}`);await sleep(10000);
} else console.log(` (closed ${st.app1Close})`);
// ===== APP2 — NEGATIVE PROBE: wrong nonce at reveal must FAIL =====
step('APP2 — seed B\'s application');
await seedApp(appB,'app2Seed');
step('APP2 — mint ballot to A (chamber 0)');
await mintBallot(balA,'A2'); // a fresh zero-ballot at balA (separate UTXO from A1's revealed ballot)
step('APP2 — A commits a YES vote with nonce N1');
await commitVote(balA,A,sig.a,cPub.a,pkhHex.a,'app2CommitA',0,nonces.aApp2);
step('NEGATIVE — A tries to reveal with WRONG nonce (must FAIL)');
let negativeOk=st.app2NegProbed===true;
if(!negativeOk){
try {
const a=await anyNftAt(appB.tokenAddress,appCat);if(!a)throw new Error('neg: app UTXO missing');
const all=await provider.getUtxos(balA.tokenAddress);
const b=all.find(x=>x.token?.category===balCat&&x.token?.nft?.capability==='mutable'&&x.token.nft.commitment!==ZERO32&&!x.token.nft.commitment.startsWith('00'.repeat(31)));
if(!b){console.log(' (committed ballot already consumed — skipping)');negativeOk=true;}
else {
const before=decodeAppCommit(a.token.nft.commitment);
let {rYes,rNo,dYes,dNo,rAtt,dAtt,spare}=before;
rAtt=rAtt+1n;rYes=rYes+1n; // pretend a successful YES (which the covenant should reject)
const newCommit=appCommit(rYes,rNo,dYes,dNo,rAtt,dAtt,spare);
const appBefore=BigInt(a.satoshis);
const appAfter=appBefore-CITIZEN_REWARD;
const revealedBallotCommit='00'+'00'.repeat(31); // YES = 0
const {p,t}=await pickW(A,MF+1000n);
const tb=new TransactionBuilder({provider});
tb.addInput(a,appB.unlock.revealVote(0n));
// PASS WRONG NONCE (aApp2WrongReveal instead of aApp2) — covenant must reject
tb.addInput(b,balA.unlock.reveal(0n,hexToBin(nonces.aApp2WrongReveal),cPub.a,sig.a));
for(const x of p)tb.addInput(x,sig.a.unlockP2PKH());
tb.addOutput({to:appB.tokenAddress,amount:appAfter,token:{amount:0n,category:appCat,nft:{capability:'mutable',commitment:newCommit}}});
tb.addOutput({to:balA.tokenAddress,amount:BAL_DUST,token:{amount:0n,category:balCat,nft:{capability:'mutable',commitment:revealedBallotCommit}}});
tb.addOutput({to:A.cashaddr,amount:t+CITIZEN_REWARD-MF});
await tb.send();
console.log(` UNEXPECTED PASS — wrong-nonce reveal should have failed`);
}
} catch (e) {
console.log(` EXPECTED FAIL — wrong-nonce reveal rejected (${(e?.message||e).toString().slice(0,90)}...)`);
negativeOk=true;
}
st.app2NegProbed=negativeOk;save(st);
} else console.log(` (negative probe already recorded as ${negativeOk?'expected-fail':'unexpected-pass'})`);
// ===== VERIFY =====
step('VERIFY');
const checks=[
['app-state + ballot categories genesised', !!appCat && !!balCat],
['APP1 — seeded with 20000 sats escrow', !!st.app1Seed],
['APP1 — ballots minted to A (chamber 0) and B (chamber 1)', !!st.balMintA1 && !!st.balMintB1],
['APP1 — A & B COMMITTED their votes (hash-bound, direction secret)', !!st.app1CommitA && !!st.app1CommitB],
['APP1 — A REVEALED YES (regular): counter rYes 0->1, A paid 4000', !!st.app1RevealA],
['APP1 — B REVEALED YES (delegated): counter dYes 0->1, B paid 6000', !!st.app1RevealB],
['APP1 — close() executed, A took 10000 sat residual', !!st.app1Close],
['APP2 — seeded with 20000 sats escrow', !!st.app2Seed],
['APP2 — A2 ballot minted at balA address (fresh zero ballot)', !!st.balMintA2],
['APP2 — A committed YES vote with nonce N1', !!st.app2CommitA],
['NEGATIVE — wrong-nonce reveal rejected by covenant', negativeOk],
];
let ok=true;for(const [l,p] of checks){console.log(` [${p?'PASS':'FAIL'}] ${l}`);ok=ok&&p;}
step('SUMMARY — Phase E: commit-reveal secret ballot (§6.9 public who, private how)');
console.log(` appCat ${appCat}\n balCat ${balCat}`);
console.log(` Ballot(A) ${balA.tokenAddress}`);
console.log(` Ballot(B) ${balB.tokenAddress}`);
console.log(` AppE(D) ${appD.tokenAddress}`);
console.log(` AppE(B) ${appB.tokenAddress}`);
console.log(' ');
console.log(' Each vote is now TWO transactions:');
console.log(' COMMIT — voter sets ballot commit = sha256(choice||nonce||pkh)');
console.log(' (the chain sees the hash; vote direction stays secret)');
console.log(' REVEAL — voter co-spends Application + Ballot, publishes preimage');
console.log(' BallotCovenant.reveal verifies sha256 binding + rewrites');
console.log(' commit as choice_byte ++ 31 zeros, ApplicationCovenantE.');
console.log(' revealVote reads output[1].commitment to learn the choice,');
console.log(' applies all Phase D guards + escrow drain.');
console.log(' Time gates (commit/reveal windows) are present in the contracts but');
console.log(' NOT exercised here to keep wall-clock manageable. Phase F+ adds full');
console.log(' CLTV-bracketed timing tests.');
console.log(' Phase E scope only — Elder/founder tier + complaint-spam + lazy-voter');
console.log(' (F), wallet labels (G) remain ahead in the §6.15 roadmap.');
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);});