Files
teahubxyzandClaude Opus 5.5 5a7f11e49d Orgchain v1.14.1: a belt counts only on its seat's own sat
Placement is now a rule of lineage/v0, not just how Orgchain inscribes:
an award counts only if it is inscribed on its recipient's own sat, so any
indexer can check that a belt is soulbound. The reducer compares sats
(satOf): the lab's sat numbers, ord's, or, without ord's sat index, its
satpoints, which two inscriptions share exactly when they share a sat. ord
events now carry the sat number when ord has one. Belts inscribed anywhere
else, as before v1.14, are rejected.

A promotion is another reinscription on the same sat, over the belt before
it: each badge keeps its skill's earlier belts, and Inspect shows the
ladder ("re-inscribed over white (beat 5)"). The sample's sol now goes
white, then green. An award and its recipient's own close no longer
conflict within a block, since both spend the recipient's inscription.

Rehearsed again on Bitcoin Core regtest from height 178: sixteen beats and
133 transactions; ord shows sol's sat carrying sol, its white belt and its
green belt, in order, each belt charmed reinscription and vindicated, and
the compare matches. The fixture is re-recorded.

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

142 lines
8.6 KiB
JavaScript

// Rehearse the yolopreneur sample on real Bitcoin Core (regtest), with its deals and the trees that change hands. 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, adopt, deposit, offer, sell, send, take } from '../../src/orgchain/actions.js';
import { askId } from '../../src/orgchain/offers.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, 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' });
act(lab, 'keyring', yolo, {});
await beat("well-known and time, and yolopreneur's keyring");
send(lab, yolo, time, 40 * SAT);
send(lab, yolo, wk, 10 * SAT);
const mailr = 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);
const [docscope] = ['docscope', 'dollhouse', 'doodleloop', 'hearthframe'].map(name => act(lab, 'birth', time, { name, kind: 'org' }));
act(lab, 'award', time, { to: sol, skill: 'launch', belt: 'white' }); // green next beat, re-inscribed over it on sol's sat
await beat("time's ventures, and sol's white belt");
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.' });
act(lab, 'keyring', sol, {});
act(lab, 'keyring', lineage, {}); // retired at the next beat, when lineage graduates and its keys change
await beat('belts, a decision, an item and two keyrings');
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);
act(lab, 'keyring', lineage, {}); // its new keys, a beat after the graduation
await beat("another holding, and lineage's new keyring");
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' });
act(lab, 'keyring', scout, {});
await beat("scout pays well-known, which awards it a belt, and scout's keyring");
// Both sides of a deal (v1.12). An ask: well-known signs mailr's input SIGHASH_SINGLE|ANYONECANPAY, and lineage takes
// it behind a dummy of its own, then adopts mailr, which joins it and moves to its new keys. An offer: acme bids for
// docscope, time accepts, and acme adopts it a beat later, while scout takes lineage's ask for the launch kit.
take(lab, askId(sell(lab, mailr, SAT)), lineage);
await beat('lineage takes the ask for mailr and adopts it');
accept(lab, offer(lab, docscope, acme, 0.5 * SAT).txid);
await beat('acme buys docscope on an offer time accepts');
adopt(lab, docscope, acme);
take(lab, askId(sell(lab, kit, 0.2 * SAT)), scout);
await beat('acme adopts docscope, and scout takes the ask for the launch kit');
// Whole trees (v1.13). An offer: acme bids for time with its six below it, every coin they sit in as an input, and
// yolopreneur accepts cash-free; acme adopts it a beat later, and the coin it bought splits back into the tree at
// acme's keys. An ask: acme's owner lists acme whole, its tree bundled into one coin (eleven inscriptions, the launch
// kit scout holds among them), and lineage takes it, adopts it, and splits it back at lineage's keys.
deposit(lab, acme, 10 * SAT);
accept(lab, offer(lab, time, acme, SAT).txid);
await beat('acme buys time with its tree, on an offer yolopreneur accepts');
adopt(lab, time, acme);
await beat("acme adopts time, and the tree splits back at acme's keys");
deposit(lab, lineage, 5 * SAT);
const whole = sell(lab, acme, 2 * SAT);
await beat("acme's owner lists acme whole, its tree in one coin");
take(lab, askId(whole), lineage);
await beat('lineage takes acme whole and adopts it');
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}${r.moved ? ' · moved' : r.away ? ' · graduated' : ''}`)).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}`);