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
This commit is contained in:
teahubxyzandClaude Opus 5.5 committed 2026-10-15 02:04:35 -07:00
1 parent 7d981341ff
commit 7c9306402d
21 files changed
+602 -21

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
# Regtest rehearsal
Lineage is an ordinals metaprotocol (`SPEC.md`). This folder checks that on real Bitcoin Core and real `ord`: the lab's
own action code makes the yolopreneur sample, Bitcoin Core (regtest, standardness on) accepts its transactions byte
for byte, `ord` indexes the inscriptions, and the same reducer folds ord's view back into the same trees.
Regtest is local and private: nothing is downloaded, blocks are mined on demand, and the data folders stay a few MB.
Test coins only.
## Start
```
brew install bitcoin ord
mkdir -p ~/lineage-regtest/bitcoin ~/lineage-regtest/ord
cp tools/regtest/bitcoin.conf ~/lineage-regtest/bitcoin/
bitcoind -datadir=$HOME/lineage-regtest/bitcoin -daemon
bitcoin-cli -datadir=$HOME/lineage-regtest/bitcoin createwallet funder
ord --regtest --bitcoin-data-dir $HOME/lineage-regtest/bitcoin --data-dir $HOME/lineage-regtest/ord \
server --address 127.0.0.1 --http-port 8088 --polling-interval 1s &
```
`bitcoin.conf` keeps mainnet's standardness rules (`acceptnonstdtxn=0`) and lowers the minimum relay fee to 0.1 sat/vB,
since the lab pays a fixed 500 sats per transaction.
## Rehearse, index, compare
```
node tools/regtest/rehearse.mjs # nine beats; saves the lab to tools/regtest/out/lab.json
node bin/lineage-index.mjs --from 100 --compare tools/regtest/out/lab.json
node tools/regtest/record.mjs # refresh tests/fixtures/regtest-rehearsal.json (public data only)
```
The rehearsal funds the lab wallet with real regtest coins (`LocalChain.importCoin`) and pays every local beat to an
unspendable key, so nothing it signs spends a coin regtest doesn't have. After each beat it broadcasts that beat's
transactions and mines one regtest block. `--compare` checks that every event landed in the same beat with the same
metadata and parent, that every inscription sits in the same output, and that both fold into the same trees. Within a
block the miner may order independent events differently from the lab; that changes nothing (`SPEC.md`, section 2).
`out/` holds the lab backup, test keys included, and is not committed.
## Stop
```
bitcoin-cli -datadir=$HOME/lineage-regtest/bitcoin stop
pkill -f "ord --regtest"
```
+13
View File
@@ -0,0 +1,13 @@
regtest=1
server=1
txindex=1
fallbackfee=0.0001
[regtest]
rpcbind=127.0.0.1
rpcallowip=127.0.0.1
rpcport=18443
port=18444
listen=0
acceptnonstdtxn=0
minrelaytxfee=0.000001
blockmintxfee=0.000001
+16
View File
@@ -0,0 +1,16 @@
// Record what ord answered about a rehearsal, with the lab's public chain (no keys), as a fixture the unit tests replay
// without bitcoind or ord: tests/fixtures/regtest-rehearsal.json. Usage: node tools/regtest/record.mjs [lab.json] [from]
import { readFileSync, writeFileSync } from 'node:fs';
import { decodeLab } from '../../src/orgchain/store.js';
import { ordClient, ordEvents } from '../../src/indexer/ord.js';
const base = 'http://127.0.0.1:8088', from = Number(process.argv[3] ?? 120), ord = {};
const lab = decodeLab(readFileSync(process.argv[2] ?? 'tools/regtest/out/lab.json', 'utf8'));
const record = async (url, options) => {
const res = await fetch(url, options), body = await res.text();
ord[new URL(url).pathname] = { status: res.status, body };
return new Response(body, { status: res.status });
};
const events = await ordEvents(ordClient(base, record), { from });
writeFileSync('tests/fixtures/regtest-rehearsal.json', `${JSON.stringify({ from, ord, chain: lab.chain.snapshot() })}\n`);
console.log(`recorded ${Object.keys(ord).length} ord answers for ${events.length} events, and ${lab.chain.blocks.length} local blocks`);
+107
View File
@@ -0,0 +1,107 @@
// 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}`);
+20
View File
@@ -0,0 +1,20 @@
// A small JSON-RPC client for the local regtest bitcoind (tools/regtest/README.md), authenticated with its cookie.
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
export const DATADIR = process.env.LINEAGE_REGTEST ? join(process.env.LINEAGE_REGTEST, 'bitcoin') : join(homedir(), 'lineage-regtest', 'bitcoin');
export function bitcoind({ url = 'http://127.0.0.1:18443', datadir = DATADIR, wallet = null } = {}) {
let id = 0;
return async (method, ...params) => {
const cookie = readFileSync(join(datadir, 'regtest', '.cookie'), 'utf8').trim();
const res = await fetch(wallet ? `${url}/wallet/${wallet}` : url, {
method: 'POST', headers: { Authorization: `Basic ${Buffer.from(cookie).toString('base64')}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '1.0', id: ++id, method, params }),
});
const body = await res.json().catch(() => null);
if (!body || body.error) throw new Error(`${method}: ${body?.error?.message ?? `HTTP ${res.status}`}`);
return body.result;
};
}