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
113 lines
6.6 KiB
JavaScript
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}`);
|