chore: checkpoint in-flight work across domains Sweep of accumulated uncommitted work on design/dex-p2p-architecture into one checkpoint (multiple sessions share this tree). Main areas: - anthemus: covenant-wiring follow-ups (engine, ExchangePanel), AgoraScreen seam - spv: ServerPool + p2p header transport (pool.ts, core/spv/*), wallet.ts wiring - walletconnect: responder/useConnect updates + connect-responder-compat test - wallet: send-tier-a rework + swap.ts, plus send-tier-a unit tests - asm: chipnet throughput/stress harness (scripts/chipnet-asm-*) + ASM_Native docs - app UI: screens/* and shell.css touch-ups; sat -> bits copy migration - docs: new snapshots (INDEX regenerated), Parameters (WalletConnect/SSH/ASM), KnowledgeBase, WALLET-COORDINATION changelog Checkpoint only; not a release. Secrets (Keys/, .chipnet-keys*, .env) remain gitignored and were verified absent from the staged set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> @
136 lines
8.4 KiB
JavaScript
136 lines
8.4 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* chipnet-asm-multi-reservoir.mjs — one backed asset (ONE category), K reservoir
|
||
* shards, with auto-assignment. The throughput layer on top of the sequencer.
|
||
*
|
||
* Setup: genesis ONE category whose supply is split across K reservoir UTXOs
|
||
* (K minting-NFT shards), all at the same contract address. The claim() covenant
|
||
* is per-UTXO, so each shard is independently claimable — same asset, one listing.
|
||
*
|
||
* Serve: N swap requests arrive; the auto-assigner hands each the next FREE shard.
|
||
* Up to K requests broadcast TRULY IN PARALLEL (different reservoir coins + their
|
||
* own funding → no shared input → no collision). Overflow (N>K) chains within its
|
||
* shard (the per-shard sequencer). Result: K settlements per round instead of 1.
|
||
*
|
||
* Users fund their OWN claim here (each provides its BCH input + signs it), so a
|
||
* round is genuinely parallel with nothing shared. Chipnet only.
|
||
*/
|
||
import { readFileSync } from 'node:fs';
|
||
import { join } from 'node:path';
|
||
import { TestNetWallet } from 'mainnet-js';
|
||
import { Contract, ElectrumNetworkProvider, TransactionBuilder, SignatureTemplate } from 'cashscript';
|
||
|
||
const ROOT = join(import.meta.dirname, '..');
|
||
const ARTIFACT = JSON.parse(readFileSync(join(ROOT, 'contracts/dist/DeviantOpenMintFT.json'), 'utf8'));
|
||
const K = Number(process.env.K ?? '3'); // reservoir shards
|
||
const USERS = (process.env.USERS ?? 'c,e,f,g,b,d').split(',').map((s) => s.trim().toLowerCase());
|
||
const FUNDER = (process.env.FUNDER ?? 'd').toLowerCase(); // genesis funder + founder
|
||
const RATE = 1000n, FEE_BPS = 100n, MINER = 3000n, AMOUNT = 60n;
|
||
const PER_SHARD_SUPPLY = 1_000_000_000n;
|
||
|
||
const KEY = (w) => join(ROOT, w === 'a' ? '.chipnet-keys.json' : `.chipnet-keys-${w}.json`);
|
||
const 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; };
|
||
const binToHex = (b) => Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
|
||
const rev = (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; };
|
||
const p2 = (pkh) => { const o = new Uint8Array(25); o[0] = 0x76; o[1] = 0xa9; o[2] = 0x14; o.set(pkh, 3); o[23] = 0x88; o[24] = 0xac; return o; };
|
||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||
|
||
async function main() {
|
||
const provider = new ElectrumNetworkProvider('chipnet');
|
||
const { wif } = JSON.parse(readFileSync(KEY(FUNDER), 'utf8'));
|
||
const funder = await TestNetWallet.fromWIF(wif);
|
||
const funderSig = new SignatureTemplate(funder.privateKey);
|
||
const funderLB = p2(funder.publicKeyHash);
|
||
|
||
// ── SETUP: genesis ONE category split across K reservoir shards ──
|
||
console.log(`\n── SETUP: genesis 1 category → ${K} reservoir shards ──`);
|
||
// 1) self-send to guarantee a vout=0, token-free UTXO (genesis must spend vout=0).
|
||
const ss = await funder.send([{ cashaddr: funder.cashaddr, value: 40000, unit: 'sat' }]);
|
||
const ssTxid = ss?.txId ?? ss?.txid;
|
||
const category = ssTxid; // CHIP-2022-02: category = input[0] prevout txid (vout 0)
|
||
await sleep(1500);
|
||
const contract = new Contract(ARTIFACT, [rev(category), RATE, FEE_BPS, funderLB], { provider });
|
||
console.log(` category ${category.slice(0, 16)}… contract ${contract.tokenAddress.slice(0, 24)}…`);
|
||
|
||
const genIn = { txid: ssTxid, vout: 0, satoshis: 40000n };
|
||
const genChange = 40000n - BigInt(K) * 1000n - 1500n;
|
||
const gtb = new TransactionBuilder({ provider });
|
||
gtb.addInput(genIn, funderSig.unlockP2PKH());
|
||
for (let k = 0; k < K; k++) {
|
||
gtb.addOutput({ to: contract.tokenAddress, amount: 1000n, token: { amount: PER_SHARD_SUPPLY, category, nft: { capability: 'minting', commitment: '' } } });
|
||
}
|
||
gtb.addOutput({ to: funder.cashaddr, amount: genChange });
|
||
const gsent = await gtb.send();
|
||
const genTxid = typeof gsent === 'string' ? gsent : gsent?.txid ?? gsent?.txId;
|
||
console.log(` genesis ${genTxid.slice(0, 16)}… → ${K} shards of ${PER_SHARD_SUPPLY} each`);
|
||
|
||
// Derive the K shard reservoir UTXOs locally (outputs 0..K-1 of the genesis tx).
|
||
const shards = [];
|
||
for (let k = 0; k < K; k++) {
|
||
shards.push({ id: k, remaining: PER_SHARD_SUPPLY, reservoir: { txid: genTxid, vout: k, satoshis: 1000n, token: { amount: PER_SHARD_SUPPLY, category, nft: { capability: 'minting', commitment: '' } } } });
|
||
console.log(` shard ${k}: ${genTxid.slice(0, 10)}…:${k}`);
|
||
}
|
||
await sleep(2000);
|
||
|
||
// ── SERVE: N requests, auto-assigned across shards, K in parallel per round ──
|
||
const users = [];
|
||
for (const u of USERS) {
|
||
const { wif: w } = JSON.parse(readFileSync(KEY(u), 'utf8'));
|
||
const wal = await TestNetWallet.fromWIF(w);
|
||
users.push({ label: u.toUpperCase(), wal, sig: new SignatureTemplate(wal.privateKey), lb: p2(wal.publicKeyHash), tokenaddr: wal.tokenaddr, cashaddr: wal.cashaddr });
|
||
}
|
||
const gross = AMOUNT * RATE, fee = (gross * FEE_BPS) / 10_000n;
|
||
|
||
console.log(`\n── SERVE: ${users.length} requests arrive at once → auto-assign to ${K} shards ──`);
|
||
console.log(`(single reservoir would settle 1 per round; ${K} shards settle up to ${K} per round, in parallel)\n`);
|
||
|
||
// pre-fetch each user's funding UTXO (their own BCH)
|
||
for (const u of users) {
|
||
u.bch = (await provider.getUtxos(u.cashaddr)).filter((x) => !x.token).sort((a, b) => Number(b.satoshis) - Number(a.satoshis))[0];
|
||
}
|
||
|
||
const rounds = Math.ceil(users.length / K);
|
||
const settled = [];
|
||
for (let r = 0; r < rounds; r++) {
|
||
const batch = users.slice(r * K, r * K + K).map((u, j) => ({ u, shard: shards[j] })); // auto-assign: request → next free shard
|
||
const t0 = Date.now();
|
||
console.log(`round ${r + 1}: [${batch.map((b) => `${b.u.label}→shard${b.shard.id}`).join(', ')}] (broadcast in parallel)`);
|
||
const res = await Promise.all(batch.map(async ({ u, shard }) => {
|
||
const at = Date.now() - t0;
|
||
try {
|
||
const change = 1000n + BigInt(u.bch.satoshis) - 1000n - gross - fee - MINER;
|
||
if (change < 546n) throw new Error('user out of BCH');
|
||
const newRemaining = shard.remaining - AMOUNT;
|
||
const tb = new TransactionBuilder({ provider });
|
||
tb.addInput(shard.reservoir, contract.unlock.claim(AMOUNT, u.lb));
|
||
tb.addInput(u.bch, u.sig.unlockP2PKH());
|
||
tb.addOutput({ to: contract.tokenAddress, amount: 1000n, token: { amount: newRemaining, category, nft: { capability: 'minting', commitment: '' } } });
|
||
tb.addOutput({ to: u.tokenaddr, amount: gross, token: { amount: AMOUNT, category } });
|
||
tb.addOutput({ to: funder.cashaddr, amount: fee });
|
||
tb.addOutput({ to: u.cashaddr, amount: change });
|
||
const spent = `${shard.reservoir.txid.slice(0, 10)}…:${shard.reservoir.vout}`;
|
||
const sent = await tb.send();
|
||
const txid = typeof sent === 'string' ? sent : sent?.txid ?? sent?.txId;
|
||
// advance this shard + this user's funding for any later round
|
||
shard.reservoir = { txid, vout: 0, satoshis: 1000n, token: { amount: newRemaining, category, nft: { capability: 'minting', commitment: '' } } };
|
||
shard.remaining = newRemaining;
|
||
u.bch = { txid, vout: 3, satoshis: change };
|
||
return { label: u.label, shard: shard.id, at, txid, spent, err: null };
|
||
} catch (e) {
|
||
return { label: u.label, shard: shard.id, at, txid: null, err: (e?.message ?? String(e)).replace(/\s+/g, ' ').slice(0, 100) };
|
||
}
|
||
}));
|
||
res.forEach((x) => console.log(` ${x.label} shard${x.shard} +${x.at}ms spent ${x.spent ?? '—'} ${x.txid ? '✓ ' + x.txid.slice(0, 16) + '…' : '✗ ' + x.err}`));
|
||
settled.push(...res);
|
||
await sleep(1500);
|
||
}
|
||
|
||
const ok = settled.filter((x) => x.txid).length;
|
||
const conflicts = settled.filter((x) => x.err?.includes('mempool-conflict')).length;
|
||
console.log(`\n=== MULTI-RESERVOIR RESULT ===`);
|
||
console.log(` ${ok}/${settled.length} requests settled across ${K} shards of ONE category — ${conflicts} collisions.`);
|
||
console.log(` Up to ${K} settled truly in parallel per round (different shard coins + own funding).`);
|
||
console.log(` Throughput = K shards × ~250 chain-depth per block. Auto-assign spreads load; a new`);
|
||
console.log(` user is handed the next free shard, so simultaneous swaps never fight over one coin.`);
|
||
}
|
||
main().then(() => process.exit(0)).catch((e) => { console.error('ERROR:', e?.message ?? e); if (e?.stack) console.error(e.stack.split('\n').slice(0, 3).join('\n')); process.exit(1); });
|