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
108 lines
6.3 KiB
JavaScript
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}`);
|