Files
orgchain/tools/regtest/rehearse.mjs
T
teahubxyzandClaude Opus 5.5 6998ae47aa Orgchain v1.10: keyrings, each key signing in its own chain's standard
A new lineage/v0 op binds a node's keys in one inscription whose parent is the
node itself: its Taproot and SegWit addresses, its npub and its Ethereum
address. Every key signs the same statement the way its own wallets sign a
message: BIP-322 simple signatures for the Bitcoin addresses, a NIP-01 note
(kind 1, created_at 0) for the npub, EIP-191 for Ethereum. The reducer checks
every proof, binds a standing keyring's keys to its node, lets the newest
keyring replace the older, and retires keyrings at a graduation (the keys
change with the new root) and at a close.

Every account gets an Ethereum key at m/44'/60'/account'/0/0; an org's, at
account 0, is the key tree page's legal-control key and matches its published
vector. Keyring is the ninth verb; Inspect shows each node's keyring and the
statement its keys signed, and Reveal its Ethereum private key. The sample
publishes three keyrings, and lineage's retires when it graduates.

Tests pin BIP-322's published vectors and values cross-checked with
bip322-js, ethers and nostr-tools. The regtest rehearsal, rerun with five
keyrings, confirmed 89 transactions on Bitcoin Core; ord's 26 events fold to
the same trees and keyrings, every proof checked from ord's copy, and 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 10:48:28 -07:00

113 lines
6.6 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' });
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);
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.' });
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");
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}`);