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