deviant/scripts/chipnet-asm-multi-reservoir.mjs
Local Dev ced2eda48d @
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>
@
2026-08-14 02:00:43 +02:00

136 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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); });