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:
1 parent
7d981341ff
commit
7c9306402d
21 files changed
+602
-21
No files matched your search
@@ -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"
|
||||
```
|
||||
@@ -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
|
||||
@@ -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`);
|
||||
@@ -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}`);
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user