Orgchain v1.1: lineage's keys, from the tree branch

Folds in the tree branch's key layer (2ea7884): lineage-keys.js and
handoff.js come over as written, with their spec vectors; the off-chain
keyring and the old UI stay behind, because v1's tree comes from the chain.

- Each holding gets its own 24-word lab root. Every node has lineage's
  keys at its place in the tree: BIP-85 org seeds, agents as accounts
  1..n of their org's seed, BIP-86 wallets and NIP-06 npubs.
- Every act is signed by whichever key holds the authorizing inscription;
  a birth hands the child its own; the lab wallet is the faucet.
- Graduation is a claim by the org's fresh root, countersigned by its
  derived key, and the derived subtree re-keys under the new root, so the
  parent's line can no longer sign for it. Adopted descendants keep theirs.
- Inspect shows each node's key path, npub, address and current holder,
  and which key signed each event; the act panel names the signing key.
- Labs saved before keys still load; their holdings sign with the lab
  wallet.
- 49 unit tests (6 new), Playwright 6/6 with key checks.

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-14 18:22:22 -07:00
1 parent 6acc319ee7
commit 744c5946aa
14 files changed
+467 -19

No files matched your search

+23 -1
View File
@@ -5,6 +5,8 @@ import { MetadataIndex } from './adapters/metadata-index.js';
import { census, lineageEvents, reduce, rows, slug, summary, trail } from './orgchain/lineage.js';
import { LAB_KEY, beat, decodeLab, emptyLab, encodeLab, mutate, readLab, setTempo, tick } from './orgchain/store.js';
import { SAMPLE, act, seedSample } from './orgchain/actions.js';
import { holderOf, keyOf } from './orgchain/keys.js';
import { readEnvelope } from './adapters/inscription.js';
const $ = id => document.getElementById(id);
function h(tag, attrs = {}, ...kids) {
@@ -180,6 +182,8 @@ function renderAct() {
const sig = list => list.map(o => `${o.value}=${o.textContent}`).join('|');
if (sig(options) !== sig([...to.options])) { to.replaceChildren(...options); if (options.some(o => o.value === keep)) to.value = keep; }
$('graduate-go').textContent = `Graduate ${n.name}`;
const signer = ui.verb === 'graduate' ? parent : n, k = signer && keyOf(lab.keys, live, signer);
$('act-key').textContent = !signer || why[ui.verb] ? '' : k ? `Signs as ${signer.name}: ${k.path}` : 'Signs with the lab wallet: this holding predates lab keys.';
const below = derivedBelow(n), arming = ui.closing === n.id;
$('close-note').textContent = below
? `Closing ${n.name} spends it for the last time, and its ${plural(below, 'derived descendant')} close with it. Graduated children hold their own roots and stay open.`
@@ -207,6 +211,7 @@ function nodeView(n) {
parent && ['Edge', n.edge === 'adopted' ? `adopted at ${at(n.graduated)}: it holds its own root` : `derived: ${parent.name} holds its seed`],
n.closed !== null && ['Closed', at(n.closed)],
['Children', n.children.length ? n.children.map(id => node(id, view)?.name).join(', ') : 'none yet'],
...keyFacts(n),
['Inscription', n.id],
].filter(Boolean);
return [
@@ -220,12 +225,29 @@ function nodeView(n) {
h('li', {}, h('b', { text: d.title }), d.why && ` · ${d.why}`, h('span', { class: 'when', text: ` (${at(d.beat)})` })))))),
];
}
// Lineage's keys for a node: its derivation, npub and address as of the viewed beat, and who holds its inscription now.
const HELD = { own: 'its own key', lab: 'the lab wallet', parent: "its parent's key (its handoff is cued)", other: 'a key outside this lab' };
function keyFacts(n) {
const k = keyOf(lab.keys, view, n), holder = holderOf(lab, live, n.id);
return [['Key', k ? k.path : 'the lab wallet: this holding predates lab keys'], k && ['npub', k.npub], k && ['Address', k.wallet.address],
['Held now by', holder ? HELD[holder.who] : 'nobody: its inscription has been spent']];
}
// The key that signed an event's reveal, named when it belongs to the node that authorized it.
function signerOf(e) {
const witness = lab.chain.transactions.get(e.id.slice(0, -2))?.tx.vin.find(i => i.witness?.length === 3)?.witness;
if (!witness) return '?';
const pubkey = readEnvelope(witness).pubkey, by = node(e.parent), own = e.op === 'found' ? node(e.node) : null;
const named = [[own && keyOf(lab.keys, live, own), `${own?.name}'s root`], [by && keyOf(lab.keys, live, by), `${by?.name}'s key`],
[by?.edge === 'adopted' && keyOf(lab.keys, live, { ...by, edge: 'derived' }), `${by?.name}'s derived key, from before it graduated`]];
for (const [k, label] of named) if (k && k.wallet.pubkey === pubkey) return label;
return lab.wallet?.pubkey === pubkey ? 'the lab wallet' : `a key outside this lab (${pubkey.slice(0, 12)}…)`;
}
function eventView(e) {
const raw = index.events.find(x => x.id === e.id), body = lab.chain.transactions.get(e.id.slice(0, -2))?.inscription?.text;
const facts = [
['When', e.pending ? 'cued for the next beat' : `beat ${e.beat}`],
['Authorized by', e.parent ? `${node(e.parent)?.name ?? 'an unknown inscription'}, whose inscription this event spent` : 'nothing: a founding has no parent'],
['Inscription', e.id], body && ['Body', body], raw?.block && ['Block', raw.block],
['Signed by', signerOf(e)], ['Inscription', e.id], body && ['Body', body], raw?.block && ['Block', raw.block],
].filter(Boolean);
const entry = raw && { id: raw.id, status: raw.status, height: raw.height, block: raw.block, parent: raw.parent ?? null,
metaprotocol: raw.metaprotocol ?? null, contentType: raw.contentType, sat: raw.sat, location: raw.location };