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>
192 lines
11 KiB
JavaScript
192 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* sim-athenae.mjs — deterministic lifecycle simulation of the Athenae bicameral-
|
|
* sortition governance protocol. NOT a chipnet run (a full community lifecycle is
|
|
* hundreds of txs); this models the EXACT covenant rules + §7 adaptive formulas to
|
|
* show emergent behaviour fast and reproducibly. The covenant PRIMITIVES are proven
|
|
* tx-by-tx on chipnet (Phases A-F, EndorsementCovenant, SortitionBeacon); this sim
|
|
* exercises the composed machinery and the multi-community model.
|
|
*
|
|
* Part 1 — one community, genesis → mature: adaptive thresholds, 3-stage gating,
|
|
* sortition admissions, a poll with Elder confirmation.
|
|
* Part 2 — attacker infiltration Monte Carlo: per-sybil odds + cost, f-growth.
|
|
* Part 3 — multiple communities coexisting + cross-community isolation (mirrors the
|
|
* on-chain `require(tokenCategory == citizenCategory + 0x01)` check).
|
|
*
|
|
* Run: node scripts/sim-athenae.mjs
|
|
*/
|
|
import { writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
const OUT = join(import.meta.dirname, '..', '.sim-athenae.json');
|
|
|
|
// ---- deterministic RNG (mulberry32) ----
|
|
function rng(seed) { let a = seed >>> 0; return () => { a |= 0; a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
|
|
const bern = (r, p) => r() < p;
|
|
|
|
// ---- §7 adaptive bootstrap formulas (verbatim from the spec) ----
|
|
function adaptive(N, baseFee = 1) {
|
|
const conf = Math.min(15, Math.ceil(N / 100));
|
|
const cap = Math.max(2, 2 * conf - 1);
|
|
const pregate = conf;
|
|
const fee = Math.floor(conf * baseFee);
|
|
return { conf, cap, pregate, fee };
|
|
}
|
|
|
|
// One sortitioned chamber: draw up to `cap` jurors; each votes yes w.p. p.
|
|
// Pass when `conf` yes accumulate; fail-fast when nos exceed (cap - conf).
|
|
function chamber(r, p, conf, cap) {
|
|
let yes = 0, no = 0, draws = 0;
|
|
while (yes < conf && no <= cap - conf) { draws++; if (bern(r, p)) yes++; else no++; }
|
|
return { admitted: yes >= conf, yes, no, draws };
|
|
}
|
|
|
|
// Three-stage gate for one candidate.
|
|
function gate(r, N, cand) {
|
|
const { conf, cap } = adaptive(N);
|
|
// Stage 0 pre-gate: needs `conf` endorsements; willing endorsers ~ Binomial(2·conf, pSocial)
|
|
let endorsers = 0; for (let i = 0; i < 2 * conf; i++) if (bern(r, cand.pSocial)) endorsers++;
|
|
if (endorsers < conf) return { admitted: false, stage: 'pre-gate', endorsers, conf };
|
|
// Stage 1 random Citizens
|
|
const s1 = chamber(r, cand.pSupport, conf, cap);
|
|
if (!s1.admitted) return { admitted: false, stage: 'stage1-citizens', s1 };
|
|
// Stage 2 random Delegated (slightly stricter scrutiny)
|
|
const s2 = chamber(r, cand.pSupport * 0.97, conf, cap);
|
|
if (!s2.admitted) return { admitted: false, stage: 'stage2-delegated', s2 };
|
|
return { admitted: true, conf, cap, s1, s2 };
|
|
}
|
|
|
|
function newCommunity(name, category, opts = {}) {
|
|
return { name, category, N: 0, citizens: 0, delegated: 0, elders: 0, baseFee: opts.baseFee ?? 1,
|
|
honestShare: opts.honestShare ?? 0.95, admitted: 0, rejected: { 'pre-gate': 0, 'stage1-citizens': 0, 'stage2-delegated': 0 },
|
|
fees: 0, phaseLog: [] };
|
|
}
|
|
|
|
function sampleCandidate(r, honestShare) {
|
|
if (bern(r, honestShare)) return { kind: 'honest', pSocial: 0.7 + 0.3 * r(), pSupport: 0.55 + 0.35 * r() };
|
|
return { kind: 'sybil', pSocial: 0.05 + 0.2 * r(), pSupport: 0.1 + 0.2 * r() }; // weak social proof + low real support
|
|
}
|
|
|
|
// Grow a community from genesis to targetN.
|
|
function grow(c, r, targetN, log = true) {
|
|
// Phase 0 — founder grants the genesis cohort (viral single-sig, no gate) up to 99.
|
|
const seed0 = Math.min(99, targetN);
|
|
c.N = seed0; c.citizens = Math.floor(seed0 * 0.8); c.delegated = Math.max(2, Math.floor(seed0 * 0.18)); c.elders = 0;
|
|
if (log) console.log(` [${c.name}] Phase 0: founder grants ${seed0} (genesis cohort: ${c.citizens} citizens, ${c.delegated} delegated) — no gate, immutable schedule takes over at N=100`);
|
|
let lastPhase = 0, sybilAdmits = 0;
|
|
while (c.N < targetN) {
|
|
const cand = sampleCandidate(r, c.honestShare);
|
|
const g = gate(r, c.N, cand);
|
|
const { conf } = adaptive(c.N);
|
|
if (conf !== lastPhase) {
|
|
const a = adaptive(c.N);
|
|
c.phaseLog.push({ N: c.N, conf: a.conf, cap: a.cap, pregate: a.pregate, fee: a.fee });
|
|
if (log) console.log(` [${c.name}] N=${c.N}: conf ${a.conf}-of-${a.cap} (~${(100*a.conf/a.cap)|0}% filter) · pre-gate ${a.pregate} · fee ${a.fee}`);
|
|
lastPhase = conf;
|
|
}
|
|
if (g.admitted) {
|
|
c.admitted++; c.N++; c.citizens++; c.fees += adaptive(c.N).fee;
|
|
if (cand.kind === 'sybil') sybilAdmits++;
|
|
// organic promotions: ~12% of citizens become delegated, ~1 elder per 800 members (strict)
|
|
if (c.delegated < c.N * 0.12 && bern(r, 0.12)) { c.delegated++; c.citizens--; }
|
|
if (c.N >= 300 && c.elders < Math.floor(c.N / 800) && bern(r, 0.02)) c.elders++;
|
|
} else {
|
|
c.rejected[g.stage]++; c.fees += adaptive(c.N).fee; // application fee paid regardless (funds vouchers)
|
|
}
|
|
}
|
|
c.sybilAdmits = sybilAdmits;
|
|
return c;
|
|
}
|
|
|
|
// A poll within a mature community + Elder confirmation (adaptive scaling).
|
|
function elderConfirmRequirement(N) {
|
|
if (N >= 20000) return `20-of-39 sortitioned Elders`;
|
|
if (N >= 16000) return `16-of-30 sortitioned Elders`;
|
|
if (N >= 15000) return `15-of-29 sortitioned Elders`;
|
|
if (N >= 2000) return `2 Elder confirmations`;
|
|
if (N >= 1000) return `1 Elder confirmation`;
|
|
return `none (below N=1000)`;
|
|
}
|
|
function runPoll(c, r, trueSupport) {
|
|
const sample = Math.min(c.citizens + c.delegated, 400); // active voters this poll
|
|
let yes = 0; for (let i = 0; i < sample; i++) if (bern(r, trueSupport)) yes++;
|
|
const pass = yes * 2 > sample;
|
|
const elderReq = elderConfirmRequirement(c.N);
|
|
// Elder review: upholds when result is not anomalous (here legit). Could veto / re-vote / extend.
|
|
const upheld = true;
|
|
return { sample, yes, no: sample - yes, pass, elderReq, upheld };
|
|
}
|
|
|
|
// Cross-community isolation: mirror EndorsementCovenant's on-chain category check
|
|
// require(tx.inputs[1].tokenCategory == citizenCategory + 0x01)
|
|
function endorseAccepted(targetCommunity, endorserToken) {
|
|
return endorserToken.category === targetCommunity.category; // category-bound; foreign token rejected
|
|
}
|
|
|
|
// ============================== RUN ==============================
|
|
const out = {};
|
|
console.log('=================================================================');
|
|
console.log('ATHENAE GOVERNANCE — LIFECYCLE SIMULATION (deterministic, seed=42)');
|
|
console.log('=================================================================');
|
|
|
|
// ---- PART 1: one community to maturity ----
|
|
console.log('\n── PART 1 — One community: genesis → mature (target N=1600) ──');
|
|
const r1 = rng(42);
|
|
const c = newCommunity('BCH-Builders', 'CAT_A', { honestShare: 0.95, baseFee: 1 });
|
|
grow(c, r1, 1600);
|
|
console.log(`\n Result: N=${c.N} (${c.citizens} citizens · ${c.delegated} delegated · ${c.elders} elders)`);
|
|
console.log(` Admitted ${c.admitted} · rejected ${c.rejected['pre-gate']}@pregate / ${c.rejected['stage1-citizens']}@stage1 / ${c.rejected['stage2-delegated']}@stage2`);
|
|
console.log(` Sybil admits that slipped through: ${c.sybilAdmits} (f = ${(100*c.sybilAdmits/c.N).toFixed(3)}% of the body)`);
|
|
|
|
console.log('\n Poll at maturity — proposal with 62% true support:');
|
|
const poll = runPoll(c, r1, 0.62);
|
|
console.log(` ${poll.yes}/${poll.sample} yes → ${poll.pass ? 'PASS' : 'FAIL'} · Elder confirmation required: ${poll.elderReq} → ${poll.upheld ? 'upheld' : 'vetoed'}`);
|
|
out.part1 = { N: c.N, citizens: c.citizens, delegated: c.delegated, elders: c.elders, admitted: c.admitted, rejected: c.rejected, sybilAdmits: c.sybilAdmits, fPct: 100*c.sybilAdmits/c.N, poll };
|
|
|
|
// ---- PART 2: attacker infiltration Monte Carlo at mature params ----
|
|
console.log('\n── PART 2 — Attacker infiltration (Monte Carlo, mature N=1500, 15-of-29) ──');
|
|
const r2 = rng(1337);
|
|
const TRIALS = 20000;
|
|
let sybilPass = 0;
|
|
for (let i = 0; i < TRIALS; i++) {
|
|
const cand = { kind: 'sybil', pSocial: 0.05 + 0.2 * r2(), pSupport: 0.1 + 0.2 * r2() };
|
|
if (gate(r2, 1500, cand).admitted) sybilPass++;
|
|
}
|
|
const sybilRate = sybilPass / TRIALS;
|
|
const feePerAttempt = adaptive(1500).fee; // MBA units, non-refundable application fee
|
|
const bribePerSybil = 870; // distinct social-proof bribes/identity (§6.1.5)
|
|
console.log(` Sybil admission rate through all 3 gates: ${(sybilRate*100).toFixed(3)}% (${sybilPass}/${TRIALS})`);
|
|
console.log(` Expected attempts per admitted sybil: ${sybilRate > 0 ? (1/sybilRate).toFixed(0) : '∞'}`);
|
|
console.log(` Per-admitted-sybil cost ≈ ${sybilRate>0?(feePerAttempt/sybilRate).toFixed(0):'∞'} fee-units + ~${bribePerSybil} distinct bribes`);
|
|
const need = Math.ceil(1500 * 0.51);
|
|
console.log(` To reach f=51% of a 1500-body (~${need} sybils): ${sybilRate>0?Math.round(need/sybilRate).toLocaleString():'∞'} attempts, ~${(need*bribePerSybil).toLocaleString()} distinct bribes — and each admit faces fresh sortition`);
|
|
out.part2 = { sybilRatePct: sybilRate*100, attemptsPerSybil: sybilRate>0?1/sybilRate:null, bribesForMajority: need*bribePerSybil };
|
|
|
|
// ---- PART 3: multiple communities coexisting + isolation ----
|
|
console.log('\n── PART 3 — Multiple communities in ONE system (coexistence + isolation) ──');
|
|
const A = newCommunity('BCH-Builders', 'CAT_A', { honestShare: 0.95, baseFee: 1 });
|
|
const B = newCommunity('ArtCollective', 'CAT_B', { honestShare: 0.90, baseFee: 2 });
|
|
const C = newCommunity('OpenForum', 'CAT_C', { honestShare: 0.85, baseFee: 0 }); // loose rules
|
|
grow(A, rng(7), 1200, false); grow(B, rng(8), 700, false); grow(C, rng(9), 350, false);
|
|
for (const x of [A, B, C]) console.log(` ${x.name.padEnd(14)} cat=${x.category} N=${String(x.N).padStart(4)} conf=${adaptive(x.N).conf}-of-${adaptive(x.N).cap} citizens=${x.citizens} delegated=${x.delegated} elders=${x.elders}`);
|
|
console.log(' → Each community is its own CashToken category + its own RegistryState; thresholds, fees,');
|
|
console.log(' and membership evolve INDEPENDENTLY. They share the chain, nothing else.');
|
|
|
|
console.log('\n Cross-community isolation (mirrors on-chain require(tokenCategory == citizenCategory+0x01)):');
|
|
const tokenFromA = { category: 'CAT_A', rank: 0x01 };
|
|
const sameComm = endorseAccepted(A, tokenFromA);
|
|
const foreign = endorseAccepted(B, tokenFromA);
|
|
console.log(` A-citizen endorsing in A → ${sameComm ? 'ACCEPTED ✓' : 'rejected'}`);
|
|
console.log(` A-citizen endorsing in B → ${foreign ? 'ACCEPTED' : 'REJECTED ✓ (category mismatch — consensus-enforced)'}`);
|
|
console.log(` A poll result affects B? → NO (separate TallyState UTXOs, separate categories)`);
|
|
out.part3 = { communities: [A,B,C].map(x=>({name:x.name,cat:x.category,N:x.N,conf:adaptive(x.N).conf,cap:adaptive(x.N).cap})),
|
|
isolation: { sameCommunityAccepted: sameComm, foreignRejected: !foreign } };
|
|
|
|
console.log('\n=================================================================');
|
|
console.log('VERDICT: one community runs the full lifecycle on the adaptive machinery;');
|
|
console.log('multiple communities coexist as independent token-categories with consensus-');
|
|
console.log('enforced isolation. The protocol is a TEMPLATE instantiated per community.');
|
|
console.log('=================================================================');
|
|
|
|
writeFileSync(OUT, JSON.stringify(out, null, 2));
|
|
console.log(`\n(results written to ${OUT})`);
|