#!/usr/bin/env node // PoA reference client v0.3 — single file, zero deps. Spec: POA-SPEC-v0.1.md // CLI: init-wallet | join | send | attest | balance | verify | epoch-close | sync | checkpoint-create | checkpoint-sign // v0.3: signed state checkpoints (incremental replay), reciprocity-weighted + newcomer-dampened emission. import crypto from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; // ---------- base58 ---------- const B58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; export function b58enc(buf) { let n = 0n; for (const b of buf) n = n * 256n + BigInt(b); let s = ''; while (n > 0n) { s = B58[Number(n % 58n)] + s; n /= 58n; } for (const b of buf) { if (b === 0) s = '1' + s; else break; } return s || '1'; } export function b58dec(s) { let n = 0n; for (const c of s) { const i = B58.indexOf(c); if (i < 0) throw new Error('bad base58 char: ' + c); n = n * 58n + BigInt(i); } let hex = n.toString(16); if (hex.length % 2) hex = '0' + hex; let buf = n === 0n ? Buffer.alloc(0) : Buffer.from(hex, 'hex'); let zeros = 0; for (const c of s) { if (c === '1') zeros++; else break; } return Buffer.concat([Buffer.alloc(zeros), buf]); } // ---------- ed25519 ---------- const SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex'); const PKCS8_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex'); const pubKeyObj = raw => crypto.createPublicKey({ key: Buffer.concat([SPKI_PREFIX, raw]), format: 'der', type: 'spki' }); const privKeyObj = raw => crypto.createPrivateKey({ key: Buffer.concat([PKCS8_PREFIX, raw]), format: 'der', type: 'pkcs8' }); export function genKeypair() { const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519'); return { pubRaw: Buffer.from(publicKey.export({ type: 'spki', format: 'der' }).subarray(12)), privRaw: Buffer.from(privateKey.export({ type: 'pkcs8', format: 'der' }).subarray(16)) }; } export const addrOf = pubRaw => 'poa1' + b58enc(pubRaw); export function pubOfAddr(addr) { if (!addr || !addr.startsWith('poa1')) throw new Error('bad address: ' + addr); const raw = b58dec(addr.slice(4)); if (raw.length !== 32) throw new Error('bad address length: ' + addr); return raw; } // ---------- canonical JSON + signing ---------- export function canonical(obj) { if (Array.isArray(obj)) return '[' + obj.map(canonical).join(',') + ']'; if (obj && typeof obj === 'object') { return '{' + Object.keys(obj).sort().map(k => JSON.stringify(k) + ':' + canonical(obj[k])).join(',') + '}'; } return JSON.stringify(obj); } export const sha256hex = s => crypto.createHash('sha256').update(s).digest('hex'); export function txidOf(body) { const { sig, ...rest } = body; return sha256hex(canonical(rest)); } export function signBody(body, privRaw) { const { sig, ...rest } = body; return b58enc(crypto.sign(null, Buffer.from(canonical(rest)), privKeyObj(privRaw))); } export function verifyBody(body, signerAddr) { const { sig, ...rest } = body; if (!sig) return false; try { return crypto.verify(null, Buffer.from(canonical(rest)), pubKeyObj(pubOfAddr(signerAddr)), b58dec(sig)); } catch { return false; } } // ---------- amounts (fixed-point 6dp, BigInt micros) ---------- export function toMicros(s) { if (!/^\d+(\.\d{1,6})?$/.test(String(s))) throw new Error('bad amount: ' + s); const [w, f = ''] = String(s).split('.'); return BigInt(w) * 1000000n + BigInt((f + '000000').slice(0, 6)); } export function fromMicros(n) { return `${n / 1000000n}.${(n % 1000000n).toString().padStart(6, '0')}`; } // ---------- ledger I/O ---------- const readJSON = p => JSON.parse(fs.readFileSync(p, 'utf8')); export function writeJSON(p, o) { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(o, null, 2) + '\n'); } export const loadWallet = dir => readJSON(path.join(dir, 'wallet.json')); export function loadLedger(ledgerDir) { const params = readJSON(path.join(ledgerDir, 'params.json')); const genesis = readJSON(path.join(ledgerDir, 'genesis.json')); const txs = []; const txDir = path.join(ledgerDir, 'tx'); if (fs.existsSync(txDir)) { for (const shard of fs.readdirSync(txDir)) { for (const f of fs.readdirSync(path.join(txDir, shard))) { if (!f.endsWith('.json')) continue; txs.push({ id: f.replace('.json', ''), body: readJSON(path.join(txDir, shard, f)) }); } } } txs.sort(txCmp); const atts = {}; const attDir = path.join(ledgerDir, 'att'); if (fs.existsSync(attDir)) { for (const txid of fs.readdirSync(attDir)) { atts[txid] = fs.readdirSync(path.join(attDir, txid)) .filter(f => f.endsWith('.json')) .map(f => ({ verifier: f.replace('.json', ''), body: readJSON(path.join(attDir, txid, f)) })); } } const emissions = []; const epDir = path.join(ledgerDir, 'epochs'); if (fs.existsSync(epDir)) { for (const n of fs.readdirSync(epDir).sort((a, b) => +a - +b)) { const p = path.join(epDir, n, 'emission.json'); if (fs.existsSync(p)) emissions.push({ epoch: +n, body: readJSON(p) }); } } let checkpoint = null; const cpDir = path.join(ledgerDir, 'checkpoints'); if (fs.existsSync(cpDir)) { const files = fs.readdirSync(cpDir).filter(f => f.endsWith('.json')).sort((a, b) => parseInt(a) - parseInt(b)); if (files.length) checkpoint = readJSON(path.join(cpDir, files[files.length - 1])); } return { dir: ledgerDir, params, genesis, txs, atts, emissions, checkpoint }; } // Deterministic global tx order: ts, then sender, then NONCE (so same-second txs // from one sender replay in intent order), then id as final tiebreak. export const txCmp = (a, b) => (a.body.ts - b.body.ts) || (a.body.from < b.body.from ? -1 : a.body.from > b.body.from ? 1 : 0) || ((a.body.nonce ?? 0) - (b.body.nonce ?? 0)) || (a.id < b.id ? -1 : 1); // ---------- replay & validation ---------- // With a checkpoint, replay starts from its signed state and only applies txs AFTER // its cutoff (sorted by ts, then txid) and emissions >= covered_epochs. // Pass {full:true} for an archive-mode replay from genesis (ignores checkpoints). export function replay(ledger, extraTxs = [], opts = {}) { const balances = new Map(), nonces = new Map(), invitedBy = new Map(), joins = []; const challSeen = new Set(), challAnswers = new Set(); const escrows = new Map(); // offerId -> {maker, amount(micros), want, expires_ts} const chains = new Map(); // agent addr -> {chain: {addr, verified}} const usedRefs = new Set(); // external tx refs already used by a fill (no double-claim) const cp = (!opts.full && ledger.checkpoint) ? ledger.checkpoint : null; let startEpoch = 0; if (cp) { for (const [a, amt] of Object.entries(cp.state.balances)) balances.set(a, toMicros(amt)); for (const [a, nn] of Object.entries(cp.state.nonces)) nonces.set(a, nn); for (const [a, p] of Object.entries(cp.state.invited_by)) { invitedBy.set(a, p); joins.push({ agent: a, invited_by: p, ts: cp.ts, from_checkpoint: true }); } for (const [id, e] of Object.entries(cp.state.escrows ?? {})) escrows.set(id, { ...e, amount: toMicros(e.amount) }); for (const [a, c] of Object.entries(cp.state.chains ?? {})) chains.set(a, c); for (const r of cp.state.used_refs ?? []) usedRefs.add(r); startEpoch = cp.covered_epochs; } else { for (const [addr, amt] of Object.entries(ledger.genesis.allocations)) balances.set(addr, toMicros(amt)); } for (const em of ledger.emissions) { if (em.epoch < startEpoch) continue; for (const [addr, amt] of Object.entries(em.body.rewards || {})) { balances.set(addr, (balances.get(addr) ?? 0n) + toMicros(amt)); } } const covered = tx => cp && (tx.body.ts < cp.cutoff.ts || (tx.body.ts === cp.cutoff.ts && tx.id <= cp.cutoff.id)); const all = [...ledger.txs, ...extraTxs].sort(txCmp); const errors = [], valid = []; for (const tx of all) { if (covered(tx)) continue; // already baked into checkpoint state const b = tx.body, err = m => errors.push({ txid: tx.id, error: m }); if (txidOf(b) !== tx.id) { err('txid mismatch (file vs body hash)'); continue; } if (!verifyBody(b, b.from)) { err('bad signature'); continue; } const last = nonces.get(b.from) ?? 0; if (b.nonce !== last + 1) { err(`bad nonce ${b.nonce} (expected ${last + 1})`); continue; } if (b.type === 'transfer') { let micros; try { micros = toMicros(b.amount); } catch (e) { err(e.message); continue; } if (micros <= 0n) { err('non-positive amount'); continue; } const bal = balances.get(b.from) ?? 0n; if (bal < micros) { err(`insufficient balance ${fromMicros(bal)} < ${b.amount}`); continue; } balances.set(b.from, bal - micros); balances.set(b.to, (balances.get(b.to) ?? 0n) + micros); } else if (b.type === 'join') { if (invitedBy.has(b.from)) { err('already joined'); continue; } if (b.invited_by !== null && (typeof b.invited_by !== 'string' || !b.invited_by.startsWith('poa1'))) { err('bad invited_by'); continue; } if (b.chains !== undefined && !validChains(b.chains)) { err('bad chains map'); continue; } invitedBy.set(b.from, b.invited_by); if (b.chains) chains.set(b.from, Object.fromEntries(Object.entries(b.chains).map(([k, v]) => [k, { addr: v, verified: false }]))); joins.push({ agent: b.from, invited_by: b.invited_by, ts: b.ts, chains: b.chains ?? null }); } else if (b.type === 'chains-update') { // (re)register external chain addresses; a solana entry may carry an ed25519 // ownership proof: sig over utf8("poa-owns:"+) by the solana key. if (!validChains(b.chains) || b.chains === null) { err('bad chains map'); continue; } const entry = {}; for (const [k, v] of Object.entries(b.chains)) entry[k] = { addr: v, verified: false }; if (b.proofs && typeof b.proofs === 'object') { for (const [k, sig] of Object.entries(b.proofs)) { if (k !== 'solana' || !entry.solana || typeof sig !== 'string') continue; // only ed25519 verifiable in-protocol today try { if (crypto.verify(null, Buffer.from('poa-owns:' + b.from, 'utf8'), pubKeyObj(b58dec(entry.solana.addr)), b58dec(sig))) entry.solana.verified = true; else { err('bad solana ownership proof'); } } catch { err('unverifiable solana ownership proof'); } } } if (errors.length && errors[errors.length - 1].txid === tx.id) continue; chains.set(b.from, entry); } else if (b.type === 'swap-offer') { // maker locks POA, demanding an external-chain payment to their own address let micros; try { micros = toMicros(b.amount); } catch (e) { err(e.message); continue; } if (micros <= 0n) { err('non-positive escrow amount'); continue; } const w = b.want; if (!w || typeof w !== 'object' || !CHAIN_KEYS.includes(w.chain) || typeof w.asset !== 'string' || !w.asset.length || w.asset.length > 40 || typeof w.amount !== 'string' || !/^[0-9]+(\.[0-9]+)?$/.test(w.amount) || typeof w.to !== 'string' || w.to.length < 20 || w.to.length > 128) { err('bad want'); continue; } if (!Number.isInteger(b.expires_ts) || b.expires_ts <= b.ts || b.expires_ts > b.ts + 30 * 86400) { err('bad expires_ts'); continue; } const bal = balances.get(b.from) ?? 0n; if (bal < micros) { err(`insufficient balance for escrow`); continue; } balances.set(b.from, bal - micros); escrows.set(tx.id, { maker: b.from, amount: micros, want: w, expires_ts: b.expires_ts }); } else if (b.type === 'swap-fill') { // taker claims escrow by citing the external payment. Replay checks structure + // liveness only; the external leg is verified by ATTESTERS before countersigning // (a fill you can't verify is a fill you don't attest). Spend only FINAL fills. const off = escrows.get(b.offer); if (!off) { err('unknown, filled, or cancelled offer'); continue; } if (b.from === off.maker) { err('maker cannot self-fill'); continue; } if (b.ts > off.expires_ts) { err('offer expired'); continue; } const p = b.proof; if (!p || typeof p !== 'object' || p.chain !== off.want.chain || typeof p.ref !== 'string' || p.ref.length < 20 || p.ref.length > 128) { err('bad proof'); continue; } if (usedRefs.has(p.ref)) { err('external proof ref already used'); continue; } usedRefs.add(p.ref); balances.set(b.from, (balances.get(b.from) ?? 0n) + off.amount); escrows.delete(b.offer); } else if (b.type === 'swap-cancel') { const off = escrows.get(b.offer); if (!off) { err('unknown, filled, or cancelled offer'); continue; } if (b.from !== off.maker) { err('only maker can cancel'); continue; } if (b.ts <= off.expires_ts) { err('offer not yet expired'); continue; } balances.set(b.from, (balances.get(b.from) ?? 0n) + off.amount); escrows.delete(b.offer); } else if (b.type === 'challenge') { // semantic challenge response (v0.4). Seed is derived from the PREVIOUS epoch's // emission hash, so it is uncomputable before that epoch closes — no pre-answering. if (!Number.isInteger(b.epoch) || b.epoch < 0) { err('bad challenge epoch'); continue; } let ch; try { ch = challengeFor(ledger, b.epoch, b.from); } catch (e) { err('challenge not derivable: ' + e.message); continue; } if (b.seed !== ch.seed) { err('challenge seed mismatch'); continue; } if (typeof b.answer !== 'string' || b.answer.length < 120 || b.answer.length > 600) { err('challenge answer length out of bounds'); continue; } const low = b.answer.toLowerCase(); if (!ch.words.every(w => low.includes(w))) { err('challenge answer missing required words'); continue; } const ckey = b.from + ':' + b.epoch; if (challSeen.has(ckey)) { err('duplicate challenge for epoch'); continue; } const ahash = sha256hex('poa-ans:' + b.epoch + ':' + low.replace(/\s+/g, ' ').trim()); if (challAnswers.has(ahash)) { err('challenge answer not unique (copied)'); continue; } challSeen.add(ckey); challAnswers.add(ahash); } else { err('unsupported type ' + b.type); continue; } nonces.set(b.from, b.nonce); valid.push(tx); } return { balances, nonces, errors, valid, invitedBy, joins, escrows, chains, usedRefs, checkpoint: cp }; } export function attStatus(ledger, txid) { const K = ledger.params.K; const list = (ledger.atts[txid] || []).filter(a => a.body.txid === txid && a.body.verifier === a.verifier && verifyBody(a.body, a.verifier)); const verifiers = [...new Set(list.map(a => a.verifier))]; return { count: verifiers.length, K, final: verifiers.length >= K, verifiers }; } // ---------- builders ---------- export function buildTx(ledger, wallet, fields) { const { nonces, valid, checkpoint } = replay(ledger); let minTs = checkpoint ? checkpoint.cutoff.ts + 1 : 0; // dependency-aware ts: a tx referencing another (swap-fill/cancel -> offer) must // sort strictly AFTER it, even when clamped into the same clock second if (fields.offer) { const ref = valid.find(t => t.id === fields.offer); if (ref) minTs = Math.max(minTs, ref.body.ts + 1); } const body = { ...fields, from: wallet.address, nonce: (nonces.get(wallet.address) ?? 0) + 1, parents: valid.slice(-2).map(t => t.id), ts: Math.max(Math.floor(Date.now() / 1000), minTs) }; body.sig = signBody(body, b58dec(wallet.privkey)); return body; } export function buildAttestations(ledger, wallet, opts = {}) { const { valid } = replay(ledger); const out = []; let skippedFills = 0; for (const tx of valid) { if (tx.body.from === wallet.address) continue; if ((ledger.atts[tx.id] || []).some(a => a.verifier === wallet.address)) continue; const checks = ['sig', 'nonce', 'balance', 'ancestry']; if (tx.body.type === 'swap-fill') { // NEVER countersign an external leg you haven't verified. Callers must either // verify the external tx themselves (pass verifiedFills) or explicitly opt in. if (opts.verifiedFills?.has(tx.id)) checks.push('external-leg'); else if (opts.trustExternal) checks.push('external-leg-UNCHECKED'); else { skippedFills++; continue; } } const att = { type: 'attest', txid: tx.id, verifier: wallet.address, checks, ts: Math.floor(Date.now() / 1000) }; att.sig = signBody(att, b58dec(wallet.privkey)); out.push(att); } if (skippedFills && !opts.quiet) console.error(`note: skipped ${skippedFills} swap-fill(s) — verify the external leg first (checkSolanaLeg) or pass --trust-external-legs`); return out; } // Best-effort Solana external-leg probe for attesters: confirms the cited signature // exists, is finalized, and its account keys include want.to. Amount/asset parsing is // the attester agent's judgment call (getTransaction meta is provided raw for that). export async function checkSolanaLeg(ref, want, rpc = 'https://api.mainnet-beta.solana.com') { const r = await fetch(rpc, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'getTransaction', params: [ref, { maxSupportedTransactionVersion: 0, commitment: 'finalized' }] }) }); const j = await r.json(); if (!j.result) return { found: false }; const keys = (j.result.transaction?.message?.accountKeys ?? []).map(k => typeof k === 'string' ? k : k.pubkey); return { found: true, slot: j.result.slot, err: j.result.meta?.err ?? null, paysTo: keys.includes(want.to), meta: j.result.meta }; } // ---------- checkpoints ---------- export function checkpointCreate(ledgerDir) { const ledger = loadLedger(ledgerDir); const { balances, nonces, invitedBy, valid, escrows, chains, usedRefs } = replay(ledger); if (!valid.length) throw new Error('nothing to checkpoint'); const lastTx = valid[valid.length - 1]; const state = { balances: Object.fromEntries([...balances].filter(([, b]) => b > 0n).map(([a, b]) => [a, fromMicros(b)])), nonces: Object.fromEntries(nonces), invited_by: Object.fromEntries(invitedBy), escrows: Object.fromEntries([...escrows].map(([id, e]) => [id, { ...e, amount: fromMicros(e.amount) }])), chains: Object.fromEntries(chains), used_refs: [...usedRefs].sort() }; const height = (ledger.checkpoint?.height ?? 0) + valid.length; const cp = { height, ts: Math.floor(Date.now() / 1000), cutoff: { ts: lastTx.body.ts, id: lastTx.id }, covered_epochs: ledger.emissions.length, prev_root: ledger.checkpoint?.state_root ?? null, state, state_root: sha256hex(canonical(state)), sigs: [] }; writeJSON(path.join(ledgerDir, 'checkpoints', height + '.json'), cp); return cp; } export function checkpointSign(ledgerDir, wallet) { const cpDir = path.join(ledgerDir, 'checkpoints'); const files = fs.readdirSync(cpDir).filter(f => f.endsWith('.json')).sort((a, b) => parseInt(a) - parseInt(b)); const file = path.join(cpDir, files[files.length - 1]); const cp = readJSON(file); if (cp.sigs.some(s => s.verifier === wallet.address)) return cp; const sigBody = { type: 'checkpoint-sig', root: cp.state_root, height: cp.height, verifier: wallet.address }; sigBody.sig = signBody(sigBody, b58dec(wallet.privkey)); cp.sigs.push(sigBody); writeJSON(file, cp); return cp; } export function checkpointVerify(cp, K) { const good = cp.sigs.filter(s => s.root === cp.state_root && verifyBody(s, s.verifier)); const uniq = new Set(good.map(s => s.verifier)); return { sigs: uniq.size, quorum: uniq.size >= K }; } // ---------- semantic challenges (v0.4) ---------- // The per-identity COST leg of proof-of-agent. Each open epoch, every agent gets a // deterministic-but-unpredictable prompt (seeded by the previous epoch's emission hash) // and must submit a short original text weaving in 3 seeded words. Machine-checkable // layer: seed match, word inclusion, length, uniqueness (no copying). Semantic layer: // responses are ordinary txs needing K attestations — peers are expected to withhold // attestation from incoherent/template spam. Emission eligibility requires a FINAL // challenge response, so N sybil identities cost N real generations per epoch, forever. const WORDS = ('anchor bridge canyon copper crystal dawn ember falcon garden glacier harbor horizon island jade kernel lantern ' + 'meadow mirror nebula ocean orchard pearl prism quartz raven reef ripple river saffron signal spiral summit ' + 'thread timber tundra velvet vertex violet voyage walnut whisper willow zephyr atlas beacon cedar comet delta ' + 'echo fable forge grove haven ivory juniper karst ledge lotus marble nectar oasis onyx pigment pulse quill ' + 'relic sable sonnet sparrow terrace tide umber vault wander yarrow zenith basalt cipher dune ferrous gale ' + 'hollow inlet jetty knoll lagoon mesa nadir opal plume ridge shale trellis vine wharf').split(/\s+/); export function epochSeedOf(ledger, epoch) { if (epoch === 0) return sha256hex('poa-epoch:0:' + canonical(ledger.genesis)); const prev = ledger.emissions.find(e => e.epoch === epoch - 1); if (!prev) throw new Error(`epoch ${epoch} not open (epoch ${epoch - 1} not closed yet)`); return sha256hex('poa-epoch:' + epoch + ':' + canonical(prev.body)); } export function challengeFor(ledger, epoch, addr) { const h = sha256hex(epochSeedOf(ledger, epoch) + ':' + addr); const words = []; for (let i = 0; words.length < 3 && i < 20; i++) { const w = WORDS[parseInt(h.slice(i * 6, i * 6 + 6), 16) % WORDS.length]; if (!words.includes(w)) words.push(w); } return { epoch, seed: h.slice(0, 16), words, task: `Epoch ${epoch} challenge for ${addr}: in 120-600 characters, write an ORIGINAL short text explaining one concrete way an agent economy can resist sybil attacks. It must read coherently and naturally include all three words: "${words[0]}", "${words[1]}", "${words[2]}". Copied or template answers are rejected; incoherent answers will not be attested by peers.` }; } export function buildChallenge(ledger, wallet, answer) { const epoch = ledger.emissions.length; // current open epoch const ch = challengeFor(ledger, epoch, wallet.address); return buildTx(ledger, wallet, { type: 'challenge', epoch, seed: ch.seed, answer }); } const CHAIN_KEYS = ['solana', 'ethereum', 'base', 'bitcoin', 'tron']; export function validChains(chains) { if (chains === null) return true; if (typeof chains !== 'object' || Array.isArray(chains)) return false; return Object.entries(chains).every(([k, v]) => CHAIN_KEYS.includes(k) && typeof v === 'string' && v.length >= 20 && v.length <= 128); } // ---------- emission engine v2 ---------- // Weighted verification mining: // - reciprocity penalty: multiplier = max(floor, 1 - recipFrac), where recipFrac is the share of // your attestations whose SENDER also attested YOUR txs (self-dealing rings score ~1.0) // - newcomer dampening: first-ever rewarded epoch earns newcomer_weight_pct (default 25%) // - per-identity cap unchanged (cap_share_pct of verification pool) export function epochClose(ledgerDir) { const ledger = loadLedger(ledgerDir); const { valid, invitedBy } = replay(ledger); const n = ledger.emissions.length ? Math.max(...ledger.emissions.map(e => e.epoch)) + 1 : 0; const startEpoch = ledger.checkpoint ? ledger.checkpoint.covered_epochs : 0; const halvings = Math.floor(n / (ledger.params.halving_epochs ?? 26)); const pool = toMicros(ledger.params.E0 ?? '1000.000000') / (2n ** BigInt(halvings)); const vPool = pool * 70n / 100n, pPool = pool * 10n / 100n, cPool = pool * 20n / 100n; const gateFrom = ledger.params.challenges_from_epoch ?? Infinity; const gated = n >= gateFrom; const cap = vPool * BigInt(ledger.params.cap_share_pct ?? 25) / 100n; const floorMult = (ledger.params.diversity_floor_pct ?? 10) / 100; const newcomerMult = (ledger.params.newcomer_weight_pct ?? 25) / 100; // attestation info per verifier over post-checkpoint valid txs const info = new Map(); // v -> {count, senders: Map(sender -> count)} const attestedMe = new Map(); // sender -> Set(verifiers of their txs) for (const tx of valid) { for (const v of attStatus(ledger, tx.id).verifiers) { const e = info.get(v) ?? { count: 0, senders: new Map() }; e.count++; e.senders.set(tx.body.from, (e.senders.get(tx.body.from) ?? 0) + 1); info.set(v, e); if (!attestedMe.has(tx.body.from)) attestedMe.set(tx.body.from, new Set()); attestedMe.get(tx.body.from).add(v); } } // prior rewarded counts (post-checkpoint era) + maturity (any prior epoch ever) const prev = new Map(), matured = new Set(); for (const em of ledger.emissions) { for (const [a, c] of Object.entries(em.body.basis?.attestations || {})) { matured.add(a); if (em.epoch >= startEpoch) prev.set(a, (prev.get(a) ?? 0) + c); } } // invite-graph kinship: undirected BFS distance <= 2 (catches star/cluster topologies // that pairwise reciprocity misses at large ring sizes) const adj = new Map(); const edge = (a, b) => { if (!a || !b) return; if (!adj.has(a)) adj.set(a, new Set()); adj.get(a).add(b); }; for (const [child, parent] of invitedBy) { edge(child, parent); edge(parent, child); } const kinOf = v => { const seen = new Set([v]); let frontier = [v]; for (let hop = 0; hop < 2; hop++) { const next = []; for (const x of frontier) for (const y of adj.get(x) ?? []) if (!seen.has(y)) { seen.add(y); next.push(y); } frontier = next; } return seen; }; // eligible = agents whose challenge response for THIS epoch is FINAL counting only // attestations from graders that are (a) NON-KIN (invite-graph BFS<=2) and // (b) ESTABLISHED — themselves challenge-eligible in a PRIOR epoch (trust-weighted // grading). (a) stops a cluster grading its own spam; (b) stops null-invite sybils // (no kin edges) from bootstrapping each other: standing must be earned through // epochs of answers that passed established graders. First gated epoch has no // prior eligible set, so (b) is waived once to bootstrap the trust set. const established = new Set(); for (const em of ledger.emissions) for (const a of em.body.basis?.challenge_eligible ?? []) established.add(a); const bootstrap = established.size === 0; const eligible = new Set(); if (gated) for (const tx of valid) { if (tx.body.type !== 'challenge' || tx.body.epoch !== n) continue; const kin = kinOf(tx.body.from); const graders = attStatus(ledger, tx.id).verifiers.filter(v => !kin.has(v) && v !== tx.body.from && (bootstrap || established.has(v))); if (graders.length >= ledger.params.K) eligible.add(tx.body.from); } const delta = new Map(), weights = {}, weighted = new Map(); let totalW = 0n; for (const [v, e] of info) { if (gated && !eligible.has(v)) continue; // no answered challenge -> no verification rewards const d = e.count - (prev.get(v) ?? 0); if (d <= 0) continue; delta.set(v, d); const myAttestors = attestedMe.get(v) ?? new Set(); const kin = kinOf(v); let recip = 0, kinCnt = 0; for (const [s, c] of e.senders) { if (myAttestors.has(s)) recip += c; if (kin.has(s)) kinCnt += c; } const recipFrac = e.count ? recip / e.count : 0; const kinFrac = e.count ? kinCnt / e.count : 0; let mult = Math.max(floorMult, 1 - Math.max(recipFrac, kinFrac)); if (!matured.has(v)) mult *= newcomerMult; weights[v] = +mult.toFixed(4); const w = BigInt(Math.round(d * mult * 10000)); if (w > 0n) { weighted.set(v, w); totalW += w; } } if (totalW === 0n && !(gated && eligible.size)) return { epoch: n, skipped: true, reason: 'no new attestations' }; const rewards = new Map(), vEarned = new Map(); // challenge pool: equal split among eligible identities (a per-identity cost rebate — // paying it by weight would re-reward volume, defeating the purpose) const challOut = {}; if (gated && eligible.size) { const per = cPool / BigInt(eligible.size); for (const a of eligible) { rewards.set(a, (rewards.get(a) ?? 0n) + per); challOut[a] = fromMicros(per); } } for (const [a, w] of weighted) { let r = vPool * w / totalW; if (r > cap) r = cap; vEarned.set(a, r); rewards.set(a, (rewards.get(a) ?? 0n) + r); } const LEVELS = [10n, 3n, 1n]; const prop = new Map(); let propTotal = 0n; for (const [earner, r] of vEarned) { let up = invitedBy.get(earner) ?? null; for (const lv of LEVELS) { if (!up) break; const o = r * lv / 100n; prop.set(up, (prop.get(up) ?? 0n) + o); propTotal += o; up = invitedBy.get(up) ?? null; } } if (propTotal > pPool) for (const [a, o] of prop) prop.set(a, o * pPool / propTotal); const propOut = {}; for (const [a, o] of prop) { if (o > 0n) { rewards.set(a, (rewards.get(a) ?? 0n) + o); propOut[a] = fromMicros(o); } } const emission = { epoch: n, ts: Math.floor(Date.now() / 1000), pool: fromMicros(pool), note: gated ? 'v2.2: verification 70% (challenge-gated, reciprocity+kinship-weighted, newcomer-dampened, capped), propagation 10% (10/3/1 x3), challenges 20% (equal split among K-attested responders)' : 'v2.1: verification 70% (reciprocity+kinship-weighted, newcomer-dampened, capped), propagation 10% (10/3/1 x3), challenges 20% unallocated in prototype', basis: { attestations: Object.fromEntries(delta), challenge_eligible: gated ? [...eligible] : null }, weights, challenges: challOut, propagation: propOut, rewards: Object.fromEntries([...rewards].map(([a, r]) => [a, fromMicros(r)])) }; writeJSON(path.join(ledgerDir, 'epochs', String(n), 'emission.json'), emission); return emission; } // ---------- network mode ---------- const normURL = u => u.endsWith('/') ? u : u + '/'; export async function fetchSnapshot(nodeURL) { const r = await fetch(normURL(nodeURL) + 'api/snapshot'); if (!r.ok) throw new Error('snapshot fetch failed: ' + r.status); return r.json(); } export function writeSnapshot(snap, dir) { for (const sub of ['tx', 'att', 'epochs', 'checkpoints']) fs.rmSync(path.join(dir, sub), { recursive: true, force: true }); writeJSON(path.join(dir, 'params.json'), snap.params); writeJSON(path.join(dir, 'genesis.json'), snap.genesis); for (const t of snap.txs) writeJSON(path.join(dir, 'tx', t.id.slice(0, 2), t.id + '.json'), t.body); for (const a of snap.atts) writeJSON(path.join(dir, 'att', a.txid, a.verifier + '.json'), a.body); for (const e of snap.emissions) writeJSON(path.join(dir, 'epochs', String(e.epoch), 'emission.json'), e.body); if (snap.checkpoint) writeJSON(path.join(dir, 'checkpoints', snap.checkpoint.height + '.json'), snap.checkpoint); } export async function submitToNode(nodeURL, kind, body) { const r = await fetch(normURL(nodeURL) + 'api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ kind, body }) }); const j = await r.json().catch(() => ({})); if (!r.ok) throw new Error('submit rejected: ' + (j.error || r.status)); return j; } // ---------- CLI ---------- async function cli() { const args = process.argv.slice(2); const cmd = args[0]; const opt = (name, dflt) => { const i = args.indexOf('--' + name); return i >= 0 ? args[i + 1] : dflt; }; const flag = name => args.includes('--' + name); const node = opt('node', null); const ledgerDir = opt('ledger', node ? './poa-cache' : null); const sync = async () => { writeSnapshot(await fetchSnapshot(node), ledgerDir); }; if (cmd === 'init-wallet') { const dir = opt('dir', '.'); const wp = path.join(dir, 'wallet.json'); if (fs.existsSync(wp)) { console.error('wallet exists: ' + wp); process.exit(1); } const { pubRaw, privRaw } = genKeypair(); const wallet = { version: 1, address: addrOf(pubRaw), pubkey: b58enc(pubRaw), privkey: b58enc(privRaw), created: Math.floor(Date.now() / 1000), invited_by: opt('invited-by', null) }; writeJSON(wp, wallet); console.log(wallet.address); } else if (cmd === 'join') { const wdir = opt('wallet', '.'); if (!fs.existsSync(path.join(wdir, 'wallet.json'))) { const { pubRaw, privRaw } = genKeypair(); writeJSON(path.join(wdir, 'wallet.json'), { version: 1, address: addrOf(pubRaw), pubkey: b58enc(pubRaw), privkey: b58enc(privRaw), created: Math.floor(Date.now() / 1000), invited_by: opt('invited-by', null) }); } const w = loadWallet(wdir); if (node) await sync(); const ledger = loadLedger(ledgerDir); const chainsOpt = opt('chains', null); // JSON map of external wallet addresses, e.g. '{"solana":"...","base":"0x..."}' const fields = { type: 'join', invited_by: opt('invited-by', w.invited_by ?? null), memo: opt('memo', 'agent joining PoA network') }; if (chainsOpt) fields.chains = JSON.parse(chainsOpt); const body = buildTx(ledger, w, fields); const id = txidOf(body); if (node) { await submitToNode(node, 'tx', body); await sync(); } else writeJSON(path.join(ledgerDir, 'tx', id.slice(0, 2), id + '.json'), body); console.log(JSON.stringify({ address: w.address, join_txid: id })); } else if (cmd === 'send') { const w = loadWallet(opt('wallet')); if (node) await sync(); const ledger = loadLedger(ledgerDir); const body = buildTx(ledger, w, { type: 'transfer', to: opt('to'), amount: opt('amount'), memo: opt('memo', '') }); const id = txidOf(body); if (node) { await submitToNode(node, 'tx', body); await sync(); } else { writeJSON(path.join(ledgerDir, 'tx', id.slice(0, 2), id + '.json'), body); const after = replay(loadLedger(ledgerDir)); const bad = after.errors.find(e => e.txid === id); if (bad) { fs.rmSync(path.join(ledgerDir, 'tx', id.slice(0, 2), id + '.json')); throw new Error('rejected: ' + bad.error); } } console.log(id); } else if (cmd === 'attest') { const w = loadWallet(opt('wallet')); if (node) await sync(); const ledger = loadLedger(ledgerDir); const atts = buildAttestations(ledger, w, { trustExternal: flag('trust-external-legs') }); for (const att of atts) { if (node) await submitToNode(node, 'att', att); else writeJSON(path.join(ledgerDir, 'att', att.txid, w.address + '.json'), att); } if (node && atts.length) await sync(); console.log(`attested ${atts.length} tx(s) as ${w.address}`); } else if (cmd === 'challenge-show') { if (node) await sync(); const ledger = loadLedger(ledgerDir); const addr = opt('addr') ?? loadWallet(opt('wallet', '.')).address; console.log(JSON.stringify(challengeFor(ledger, ledger.emissions.length, addr), null, 2)); } else if (cmd === 'challenge-answer') { const w = loadWallet(opt('wallet')); if (node) await sync(); const ledger = loadLedger(ledgerDir); const body = buildChallenge(ledger, w, opt('answer')); const id = txidOf(body); if (node) { await submitToNode(node, 'tx', body); await sync(); } else { writeJSON(path.join(ledgerDir, 'tx', id.slice(0, 2), id + '.json'), body); const after = replay(loadLedger(ledgerDir)); const bad = after.errors.find(e => e.txid === id); if (bad) { fs.rmSync(path.join(ledgerDir, 'tx', id.slice(0, 2), id + '.json')); throw new Error('rejected: ' + bad.error); } } console.log(JSON.stringify({ challenge_txid: id, epoch: body.epoch })); } else if (cmd === 'swap-offer') { const w = loadWallet(opt('wallet')); if (node) await sync(); const ledger = loadLedger(ledgerDir); // TTL is relative to LEDGER time (tx ts may be clamped above wall clock by the checkpoint cutoff) const baseTs = Math.max(Math.floor(Date.now() / 1000), ledger.checkpoint ? ledger.checkpoint.cutoff.ts + 1 : 0); const body = buildTx(ledger, w, { type: 'swap-offer', amount: opt('amount'), want: { chain: opt('chain'), asset: opt('asset'), amount: opt('want-amount'), to: opt('to') }, expires_ts: baseTs + parseInt(opt('ttl', '3600'), 10), memo: opt('memo', '') }); const id = txidOf(body); if (node) { await submitToNode(node, 'tx', body); await sync(); } else writeJSON(path.join(ledgerDir, 'tx', id.slice(0, 2), id + '.json'), body); console.log(JSON.stringify({ offer_id: id, locked: body.amount, want: body.want, expires_ts: body.expires_ts })); } else if (cmd === 'swap-fill') { const w = loadWallet(opt('wallet')); if (node) await sync(); const ledger = loadLedger(ledgerDir); const body = buildTx(ledger, w, { type: 'swap-fill', offer: opt('offer'), proof: { chain: opt('chain'), ref: opt('ref') } }); const id = txidOf(body); if (node) { await submitToNode(node, 'tx', body); await sync(); } else writeJSON(path.join(ledgerDir, 'tx', id.slice(0, 2), id + '.json'), body); console.log(JSON.stringify({ fill_txid: id, offer: body.offer })); } else if (cmd === 'swap-cancel') { const w = loadWallet(opt('wallet')); if (node) await sync(); const ledger = loadLedger(ledgerDir); const body = buildTx(ledger, w, { type: 'swap-cancel', offer: opt('offer') }); const id = txidOf(body); if (node) { await submitToNode(node, 'tx', body); await sync(); } else writeJSON(path.join(ledgerDir, 'tx', id.slice(0, 2), id + '.json'), body); console.log(JSON.stringify({ cancel_txid: id, offer: body.offer })); } else if (cmd === 'swaps') { if (node) await sync(); const { escrows } = replay(loadLedger(ledgerDir)); for (const [id, e] of escrows) console.log(JSON.stringify({ offer_id: id, maker: e.maker, poa_locked: fromMicros(e.amount), want: e.want, expires_ts: e.expires_ts })); if (!escrows.size) console.log('(no open offers)'); } else if (cmd === 'chains-update') { const w = loadWallet(opt('wallet')); if (node) await sync(); const ledger = loadLedger(ledgerDir); const fields = { type: 'chains-update', chains: JSON.parse(opt('chains')) }; const proofs = opt('proofs', null); if (proofs) fields.proofs = JSON.parse(proofs); const body = buildTx(ledger, w, fields); const id = txidOf(body); if (node) { await submitToNode(node, 'tx', body); await sync(); } else writeJSON(path.join(ledgerDir, 'tx', id.slice(0, 2), id + '.json'), body); console.log(JSON.stringify({ chains_txid: id })); } else if (cmd === 'check-solana-leg') { const res = await checkSolanaLeg(opt('ref'), { to: opt('to') }, opt('rpc', 'https://api.mainnet-beta.solana.com')); console.log(JSON.stringify({ found: res.found, slot: res.slot ?? null, err: res.err ?? null, paysTo: res.paysTo ?? null })); } else if (cmd === 'sync') { await sync(); console.log('synced to ' + ledgerDir); } else if (cmd === 'balance') { if (node) await sync(); const { balances } = replay(loadLedger(ledgerDir)); const who = opt('addr', null); for (const [addr, bal] of [...balances.entries()].sort()) { if (who && addr !== who) continue; console.log(`${addr} ${fromMicros(bal)} POA`); } } else if (cmd === 'epoch-close') { console.log(JSON.stringify(epochClose(ledgerDir), null, 2)); } else if (cmd === 'checkpoint-create') { const cp = checkpointCreate(ledgerDir); console.log(JSON.stringify({ height: cp.height, state_root: cp.state_root, cutoff: cp.cutoff, covered_epochs: cp.covered_epochs, accounts: Object.keys(cp.state.balances).length })); } else if (cmd === 'checkpoint-sign') { const w = loadWallet(opt('wallet')); const cp = checkpointSign(ledgerDir, w); console.log(JSON.stringify({ height: cp.height, sigs: cp.sigs.length })); } else if (cmd === 'verify' || cmd === 'status') { if (node) await sync(); const ledger = loadLedger(ledgerDir); const full = flag('full'); const { balances, errors, valid, joins, escrows } = replay(ledger, [], { full }); if (ledger.checkpoint && !full) { const cv = checkpointVerify(ledger.checkpoint, ledger.params.K); console.log(`checkpoint: height ${ledger.checkpoint.height}, root ${ledger.checkpoint.state_root.slice(0, 12)}…, sigs ${cv.sigs}/${ledger.params.K} ${cv.quorum ? 'QUORUM' : 'NO QUORUM'}`); } console.log(`params: K=${ledger.params.K} | epochs closed: ${ledger.emissions.length} | agents known: ${joins.length}${full ? ' (full archive replay)' : ''}`); console.log(`transactions${ledger.checkpoint && !full ? ' since checkpoint' : ''}: ${valid.length} valid, ${errors.length} invalid`); for (const e of errors) console.log(` INVALID ${e.txid.slice(0, 12)}…: ${e.error}`); if (valid.length <= 40) for (const tx of valid) { const s = attStatus(ledger, tx.id); const what = tx.body.type === 'join' ? `JOIN ${tx.body.from.slice(0, 12)}…` : `${tx.body.from.slice(0, 12)}…→${(tx.body.to || '').slice(0, 12)}… ${tx.body.amount} POA`; console.log(` tx ${tx.id.slice(0, 12)}… ${what} atts ${s.count}/${s.K} ${s.final ? 'FINAL' : 'pending'}`); } const supply = [...balances.values()].reduce((a, b) => a + b, 0n); const locked = [...escrows.values()].reduce((a, e) => a + e.amount, 0n); console.log(`total supply on ledger: ${fromMicros(supply + locked)} POA${locked ? ` (${fromMicros(locked)} locked in ${escrows.size} escrow(s))` : ''}`); process.exit(errors.length ? 2 : 0); } else { console.log(`PoA client v0.4 — local mode uses --ledger DIR; network mode adds --node URL (cache in --ledger, default ./poa-cache) init-wallet --dir DIR [--invited-by ADDR] join --wallet DIR [--node URL] [--ledger DIR] [--invited-by ADDR] challenge-show [--addr ADDR | --wallet DIR] [--node URL] [--ledger DIR] challenge-answer --wallet DIR --answer "TEXT" [--node URL] [--ledger DIR] swap-offer --wallet DIR --amount POA --chain solana --asset USDC --want-amount X --to EXTADDR [--ttl SECS] swap-fill --wallet DIR --offer OFFERID --chain solana --ref EXT_TX_SIG swap-cancel --wallet DIR --offer OFFERID (only after expiry) swaps [--node URL] [--ledger DIR] (list open offers) chains-update --wallet DIR --chains JSON [--proofs JSON] check-solana-leg --ref TXSIG --to ADDR [--rpc URL] send --wallet DIR --to ADDR --amount X [--node URL] [--ledger DIR] [--memo M] attest --wallet DIR [--node URL] [--ledger DIR] balance [--addr ADDR] [--node URL] [--ledger DIR] verify [--node URL] [--ledger DIR] [--full] epoch-close --ledger DIR checkpoint-create --ledger DIR checkpoint-sign --ledger DIR --wallet DIR sync --node URL --ledger DIR`); } } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { cli().catch(e => { console.error(e.message); process.exit(1); }); }