Files
orgchain/tools/regtest/rehearse.mjs
T
teahubxyzandClaude Opus 5.5 7c9306402d Orgchain v1.9: lineage/v0 as an ordinals metaprotocol, rehearsed on regtest
SPEC.md writes lineage/v0 down as an ordinals metaprotocol: the envelope
fields, chain order, authority by verified parents, every rule, ownership by
sat. bin/lineage-index.mjs reads lineage events from a real ord server and
folds them with the same reducer the lab uses.

tools/regtest rehearses the sample on Bitcoin Core regtest with standardness
on: the lab's own action code signs on a chain funded only by coins imported
from regtest (LocalChain.importCoin), each beat's transactions are broadcast
byte for byte and mined, ord indexes them, and --compare checks the same
events in the same beats, the same metadata and parents, every inscription
in the same output and the same trees. tests/indexer.test.js replays a
recorded run.

The first rehearsal found that reveals committed to tapscript's re-encoded
tapleaf, which real Bitcoin rejects. They now commit to the exact script
bytes, and the local chain checks script paths per BIP-341/342, still
loading reveals made before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4upF5LVwmPDDqrhdUfnXh
2026-10-15 02:04:35 -07:00

108 lines
6.3 KiB
JavaScript

// Rehearse the yolopreneur sample on real Bitcoin Core (regtest). The lab's own action code signs every transaction on
// a LocalChain whose only money is coins imported from regtest, so nothing it makes spends a local coinbase. After each
// beat, that beat's transactions go to bitcoind byte for byte and are mined into a block; ord then indexes them like
// any other inscriptions, and bin/lineage-index.mjs folds them back into trees. Test coins on a private chain only.
// Usage: node tools/regtest/rehearse.mjs [out.json] (tools/regtest/README.md starts bitcoind and ord)
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path';
import { Script, Tx } from '@cmdcode/tapscript';
import { TestWallet } from '../../src/adapters/wallet.js';
import { MetadataIndex } from '../../src/adapters/metadata-index.js';
import { emptyLab, encodeLab, setRule, setTempo } from '../../src/orgchain/store.js';
import { accept, act, deposit, offer, send } from '../../src/orgchain/actions.js';
import { SAT } from '../../src/orgchain/money.js';
import { lineageEvents, reduce, rows } from '../../src/orgchain/lineage.js';
import { bitcoind } from './rpc.mjs';
const out = process.argv[2] ?? 'tools/regtest/out/lab.json';
const NUMS = ['OP_1', '50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0']; // BIP-341's key that no one holds
const hex = script => Script.encode(script, false).hex;
const node = bitcoind(), funder = bitcoind({ wallet: 'funder' });
const lab = emptyLab();
lab.wallet = TestWallet.create();
setRule(lab, 'none'); // no holding mines: the only money is what regtest sends in
setTempo(lab, 'manual'); // the rehearsal decides when a beat lands
// Local beats pay the unspendable key, so no local coinbase is ever spent; anything else beating is a bug here.
const mineTo = lab.chain.mineTo.bind(lab.chain);
lab.chain.mineTo = vout => {
if (vout.some(o => hex(o.scriptPubKey) !== hex(NUMS))) throw new Error('Out of lab money: only the rehearsal beats, and only to the unspendable key.');
return mineTo(vout);
};
const miner = await funder('getnewaddress', 'miner', 'bech32m');
const mine = n => node('generatetoaddress', n, miner);
const height = await node('getblockcount');
if (height < 120) await mine(120 - height); // mature coins for the funder, and past ord's regtest jubilee
const funding = [];
for (const btc of [20, 20, 20, 10]) funding.push(await funder('sendtoaddress', lab.wallet.address, btc));
await mine(1);
for (const txid of funding) {
const o = (await node('getrawtransaction', txid, true)).vout.find(v => v.scriptPubKey.hex === hex(lab.wallet.script));
lab.chain.importCoin({ txid, vout: o.n, value: Math.round(o.value * SAT), script: o.scriptPubKey.hex });
}
console.log(`lab wallet ${lab.wallet.address} holds ${funding.length} coins from regtest`);
let beats = 0, sent = 0;
async function beat(label) {
lab.chain.mine(NUMS);
const raws = lab.chain.blocks.at(-1).raws.slice(1);
for (const raw of raws) {
try { await node('sendrawtransaction', raw, 0); }
catch (error) {
if (!/cluster|too-long-mempool-chain/i.test(error.message)) throw new Error(`beat ${beats + 1} (${label}), ${Tx.util.getTxid(raw)}: ${error.message}`);
await mine(1); // a long chain of cued spends: let regtest confirm what it has, then carry on
await node('sendrawtransaction', raw, 0);
}
}
const [hash] = await mine(1), block = await node('getblock', hash);
for (const raw of raws) {
if (!(await node('getrawtransaction', Tx.util.getTxid(raw), true)).confirmations) throw new Error(`${Tx.util.getTxid(raw)} did not confirm.`);
}
beats++; sent += raws.length;
console.log(`beat ${beats}: ${label} · ${raws.length} transactions → regtest block ${block.height}`);
}
const yolo = act(lab, 'found', null, { name: 'yolopreneur' });
deposit(lab, yolo, 30 * SAT);
deposit(lab, yolo, 30 * SAT);
await beat('found yolopreneur and fund it');
const wk = act(lab, 'birth', yolo, { name: 'well-known', kind: 'org' }), time = act(lab, 'birth', yolo, { name: 'time', kind: 'org' });
await beat('well-known and time');
send(lab, yolo, time, 40 * SAT);
send(lab, yolo, wk, 10 * SAT);
act(lab, 'birth', wk, { name: 'mailr', kind: 'org' });
const lineage = act(lab, 'birth', wk, { name: 'lineage', kind: 'org' });
const luna = act(lab, 'birth', time, { name: 'luna', kind: 'agent' }), sol = act(lab, 'birth', time, { name: 'sol', kind: 'agent' });
act(lab, 'birth', time, { name: 'astra', kind: 'agent' });
await beat('two sends and five births');
send(lab, wk, lineage, 2 * SAT);
for (const name of ['docscope', 'dollhouse', 'doodleloop', 'hearthframe']) act(lab, 'birth', time, { name, kind: 'org' });
await beat("time's ventures");
send(lab, time, sol, SAT);
send(lab, time, luna, 0.5 * SAT);
act(lab, 'award', time, { to: sol, skill: 'launch', belt: 'green' });
act(lab, 'award', time, { to: luna, skill: 'post', belt: 'white' });
act(lab, 'decide', yolo, { title: 'Every venture gets a page', why: '<venture>.yolopreneur.lol, served from one pages branch.' });
const kit = act(lab, 'make', sol, { name: 'launch-kit', note: 'The page, the post and the checklist every venture launches with.' });
await beat('belts, a decision and an item');
act(lab, 'graduate', wk, { of: lineage });
act(lab, 'decide', lineage, { title: 'Lineage gets its own org', why: 'Its own repo on Teahub now, lineage.llc later: adopted, not derived.' });
accept(lab, offer(lab, kit, lineage, 0.1 * SAT).txid);
await beat('lineage graduates and buys the launch kit');
const acme = act(lab, 'found', null, { name: 'acme' });
deposit(lab, acme, 5 * SAT);
await beat('another holding');
const scout = act(lab, 'birth', acme, { name: 'scout', kind: 'agent' });
send(lab, acme, scout, SAT);
await beat('an agent of acme');
send(lab, scout, wk, 0.1 * SAT);
act(lab, 'award', wk, { to: scout, skill: 'listen', belt: 'white' });
await beat('scout pays well-known, which awards it a belt');
const live = reduce(lineageEvents(new MetadataIndex(lab.chain)), { pending: true });
for (const root of live.roots) console.log(rows(live, root.id).map(r => (r.type === 'group' ? `${r.guide}${r.label}` : `${r.guide}${r.path} ${r.node.name}`)).join('\n'));
mkdirSync(dirname(out), { recursive: true });
writeFileSync(out, encodeLab(lab));
console.log(`${beats} beats, ${sent} transactions confirmed on regtest (height ${await node('getblockcount')}); the lab is saved to ${out}`);