deviant/scripts/chipnet-asm-multi-reservoir.mjs

137 lines
8.4 KiB
JavaScript
Raw Normal View History

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