Orgchain v1.3: Taproot and SegWit addresses for every node

Every account now lists standard Bitcoin addresses, so no one has to
derive them: Taproot (BIP-86, m/86'/1'/account'/0/0), which holds the
node's inscription, signs its acts and receives inscriptions, and
SegWit (BIP-84, m/84'/1'/account'/0/0) for payments, beside its npub
(NIP-06). An org is account 0 of its seed and its agents are accounts
1..n, so a wallet that opens an account finds the same addresses. The
lineage key tree page derives the same branches on mainnet paths and
lists the addresses in each agent's keyring.

The v1.2 tapkey (the npub's key, tweaked) is replaced by the standard
BIP-86 key; labs keyed by v1.1 or v1.2 keep their chain keys until
they are reset. Tests pin BIP-86 and BIP-84's own vectors.

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 19:49:05 -07:00
1 parent 00a35613c5
commit 0af51dc9cd
8 files changed
+107 -62

No files matched your search

+3 -2
View File
@@ -9,7 +9,8 @@ Orgchain is lineage's app for creating and operating lineage holdings on Super T
rather than rewriting it, and keep its tests passing. Preserve upstream licenses and provenance.
- Keys follow the tree (`src/orgchain/keys.js`): store roots only (one per holding, one per graduated org) and derive
every other key from its position. Every act is signed by whichever key holds the authorizing inscription.
- A node's chain key is its tapkey: its npub with the BIP-86 tweak. Leaves are HKDF-SHA256 (salt `lineage/v0`) and
must stay byte for byte with lineage's key tree page; `tests/keys.test.js` holds values read off that page.
- A node's chain key is its account's BIP-86 Taproot key (`m/86'/1'/account'/0/0`); SegWit is BIP-84 at the same
account. Leaves are HKDF-SHA256 (salt `lineage/v0`) and must stay byte for byte with lineage's key tree page;
`tests/keys.test.js` holds values read off that page.
- The page makes no network requests; keep it that way (the deployed CSP sets `connect-src 'none'`).
- Before shipping: `npm test`, `npm run build`, `npm run test:e2e`.
+10 -9
View File
@@ -15,15 +15,16 @@ chain itself records who did what, and when.
- **One interpretation.** `src/orgchain/lineage.js` folds `lineage/v0` events into trees, and vets every act before
anything is signed. Events it rejects stay on the chain and show as rejected; they just don't change the tree.
- **Keys.** Each holding gets its own 24-word lab root, and every node has lineage's keys at its place in the tree
(`src/orgchain/keys.js`): an org's seed is BIP-85 from its parent's, or its own root once it graduates; agents are
accounts 1..n of their org's seed. A node's **tapkey** is its NIP-06 npub with the BIP-86 tweak: one secret signs its
nostr events and its chain spends, and anyone with the npub can find what it holds. Its **leaves** hang off the same
tree, byte for byte as on lineage's key tree page (`src/orgchain/leaves.js`): an agent's ssh, git, dkim and wg keys
from its nostr secret, an org's ssh-ca, mtls and noise keys from its seed, each with the public line to paste. Every
act is signed by the tapkey that holds the authorizing inscription, and a birth hands the child its own. 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. Labs keyed by v1.1 keep their separate m/86' chain keys until
they are reset.
(`src/orgchain/keys.js`): an org's seed is BIP-85 from its parent's, or its own root once it graduates; an org is
account 0 of its seed and its agents are accounts 1..n. Every account lists its addresses, so no one has to derive
them: **Taproot** (BIP-86, `m/86'/1'/account'/0/0`), which holds the node's inscription, signs its acts and receives
inscriptions; **SegWit** (BIP-84, `m/84'/1'/account'/0/0`) for payments; and its **npub** (NIP-06). Its **leaves**
hang off the same tree, byte for byte as on lineage's key tree page (`src/orgchain/leaves.js`): an agent's ssh, git,
dkim and wg keys from its nostr secret, an org's ssh-ca, mtls and noise keys from its seed, each with the public line
to paste. Every act is signed by the Taproot key that holds the authorizing inscription, and a birth hands the child
its own. 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. Labs keyed by v1.1 or v1.2 keep their
chain keys until they are reset.
Test coins and test keys only: a local chain, the lab wallet (the faucet that funds every key) and each holding's lab
root, kept in this browser (`localStorage`, key `orgchain:lab:v1`). Nothing here is a real key; minting a real root is
+7 -2
View File
@@ -1,4 +1,4 @@
// Lineage lab keys (docs/lineage.md): a BIP-85 seed per org, BIP-86 Taproot keys and NIP-06
// Lineage lab keys (docs/lineage.md): a BIP-85 seed per org; BIP-86 Taproot, BIP-84 SegWit and NIP-06
// Nostr keys per account. Lab only: the real root is the operator's call; nothing here is a real key.
import { HDKey } from '@scure/bip32';
import { generateMnemonic, mnemonicToSeedSync, entropyToMnemonic, validateMnemonic } from '@scure/bip39';
@@ -14,6 +14,8 @@ export const PATHS = {
orgSeed: index => `m/83696968'/39'/0'/24'/${index}'`,
// BIP-86 single-key Taproot. Coin type 1' (test networks) in the lab.
wallet: (account, coin = 1) => `m/86'/${coin}'/${account}'/0/0`,
// BIP-84 native SegWit (P2WPKH), the same account on the payments branch.
segwit: (account, coin = 1) => `m/84'/${coin}'/${account}'/0/0`,
// NIP-06, as on the lineage key tree page.
nostr: account => `m/44'/1237'/${account}'/0/0`,
};
@@ -54,7 +56,10 @@ export function childMnemonic(parentMnemonic, index) {
export function walletSecret(mnemonic, account = 0, coin = 1) {
return bytesToHex(master(mnemonic).derive(PATHS.wallet(assertIndex(account, 'account'), coin)).privateKey);
}
// The NIP-06 secret itself: Orgchain tweaks it into the node's tapkey (BIP-86), so one secret signs nostr and chain.
export function segwitPubkey(mnemonic, account = 0, coin = 1) {
return bytesToHex(master(mnemonic).derive(PATHS.segwit(assertIndex(account, 'account'), coin)).publicKey);
}
// The NIP-06 secret itself: the root of a node's leaves (and its chain key in labs keyed by v1.2).
export function nostrSecret(mnemonic, account = 0) {
return bytesToHex(master(mnemonic).derive(PATHS.nostr(assertIndex(account, 'account'))).privateKey);
}
+12 -8
View File
@@ -5,7 +5,7 @@ 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, leavesOf } from './orgchain/keys.js';
import { addressesOf, holderOf, keyOf, leavesOf } from './orgchain/keys.js';
import { readEnvelope } from './adapters/inscription.js';
const $ = id => document.getElementById(id);
@@ -225,8 +225,8 @@ function nodeView(n) {
keysView(n)),
];
}
// Lineage's keys for a node as of the viewed beat: its tapkey and npub, where they come from, who holds its inscription
// now, and its leaves with the public lines to paste. The leaves are the ones the lineage key tree page derives.
// Lineage's keys for a node as of the viewed beat: its Taproot, SegWit and nostr addresses, where they come from, who
// holds its inscription now, and its leaves with the public lines to paste (the ones the lineage key tree page derives).
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' };
const LEAF = { ssh: 'shell access, certified by the CA', git: 'commit signing', dkim: 'mail signing', wg: 'WireGuard peer key',
'ssh-ca': 'SSH certificate authority', mtls: 'TLS identity between nodes', noise: 'Headscale noise key' };
@@ -234,13 +234,17 @@ function keysView(n) {
const k = keyOf(lab.keys, view, n), holder = holderOf(lab, live, n.id);
const held = ['Held now by', holder ? HELD[holder.who] : 'nobody: its inscription has been spent'];
if (!k) return h('div', { class: 'keys' }, h('p', { class: 'lab', text: 'Keys' }),
h('dl', { class: 'fl' }, [['Signs with', 'the lab wallet: this holding predates lab keys, so it has no tapkey or leaves'], held].map(fact)));
const agent = n.kind === 'agent', leaves = leavesOf(lab.keys, view, n), lines = leaves.map(([, , line]) => line).filter(Boolean);
const chain = k.tapkey ? ['Tapkey', k.wallet.address, 'Its npub with the BIP-86 tweak: one secret signs its nostr events and its chain spends.']
: ['Chain key', k.wallet.address, "A separate key at m/86', the way v1.1 keyed this lab. Reset the lab to key it by tapkey."];
h('dl', { class: 'fl' }, [['Signs with', 'the lab wallet: this holding predates lab keys, so it has no addresses or leaves'], held].map(fact)));
const agent = n.kind === 'agent', a = addressesOf(lab.keys, view, n), leaves = leavesOf(lab.keys, view, n);
const lines = leaves.map(([, , line]) => line).filter(Boolean), reset = 'Reset the lab for standard BIP-86 keys.';
const holds = agent ? "Holds this seat's inscription, and inscriptions sent to it land here." : "Holds this org's inscription and signs its acts. Inscriptions sent to it land here.";
const taproot = { bip86: `BIP-86 at ${a.taproot.path}. ${holds}`, tapkey: `Its npub's key with the BIP-86 tweak, the way v1.2 keyed this lab. ${holds} ${reset}`,
wallet: `An untweaked key at ${a.taproot.path}, the way v1.1 keyed this lab. ${holds} ${reset}` }[k.scheme];
const facts = [['Taproot', a.taproot.address, taproot], ['SegWit', a.segwit.address, `BIP-84 at ${a.segwit.path}. For payments; this toy chain only moves Taproot.`],
['npub', a.npub.address, `NIP-06 at ${a.npub.path}. Its nostr name; the leaves below hang off it.`], ['From', `${k.from} · account ${k.account}`], held];
return h('div', { class: 'keys' },
h('p', { class: 'lab', text: 'Keys' }),
h('dl', { class: 'fl' }, [chain, ['npub', k.npub], ['Derived', k.path], held].map(fact)),
h('dl', { class: 'fl' }, facts.map(fact)),
h('p', { class: 'lab' }, 'Leaves', h('span', { text: ` · HKDF-SHA256 from its ${agent ? 'nostr secret' : 'seed'}` })),
h('ul', { class: 'list leaves' }, leaves.map(([label, value]) =>
h('li', {}, h('b', { text: label }), h('span', { class: 'when', text: ` ${LEAF[label]}` }), h('code', { text: value })))),
+32 -17
View File
@@ -1,24 +1,26 @@
// Lineage's key tree over Orgchain's holdings (docs/lineage.md), using the tree branch's lineage-keys.js.
// Each holding has its own 24-word lab root. An org's seed is BIP-85 from its parent's seed at its index, or its own
// root once it graduates. Agents (seats) are accounts 1..n of their org's seed. A node's tapkey is its NIP-06 npub with
// the BIP-86 tweak: one secret signs its nostr events and its chain spends, and its address follows from its npub. Its
// leaves (ssh, git, dkim, wg for agents; ssh-ca, mtls, noise for orgs) hang off the same tree. Lab only: nothing here
// is a real key.
import { Script, Tap } from '@cmdcode/tapscript';
import { PATHS, childMnemonic, nostrKey, nostrSecret, seedBytes, walletSecret } from '../adapters/lineage-keys.js';
// root once it graduates. An org is account 0 of its seed and its agents (seats) are accounts 1..n. Every account has
// standard Bitcoin addresses (BIP-86 Taproot, which holds the node's inscription and signs for it, and BIP-84 SegWit
// for payments), a NIP-06 npub, and leaves that hang off the same tree (ssh, git, dkim, wg for agents; ssh-ca, mtls,
// noise for orgs). Lab only: nothing here is a real key.
import { Address, Script, Tap } from '@cmdcode/tapscript';
import { PATHS, childMnemonic, nostrKey, nostrSecret, seedBytes, segwitPubkey, walletSecret } from '../adapters/lineage-keys.js';
import { holderCoin } from '../adapters/handoff.js';
import { TestWallet } from '../adapters/wallet.js';
import { agentLeaves, agentRecords, nodeLeaves, nodeRecords } from './leaves.js';
// 'tapkey' (from v1.2): the chain key is the npub, tweaked. 'wallet': labs keyed by v1.1, whose chain key was a
// separate, untweaked key at m/86'. A lab keeps its scheme until it is reset.
export const SCHEMES = ['tapkey', 'wallet'];
export const emptyKeys = () => ({ scheme: 'tapkey', roots: {}, adopted: {} });
// BIP-86: the output key commits to the internal key and no scripts, so anyone with an npub can find its coins.
// 'bip86' (from v1.3): the chain key is the account's BIP-86 Taproot key. Labs keyed earlier keep their scheme until
// they are reset: 'tapkey' (v1.2) tweaked the npub's key instead, and 'wallet' (v1.1) used an untweaked key at m/86'.
export const SCHEMES = ['bip86', 'tapkey', 'wallet'];
export const emptyKeys = () => ({ scheme: 'bip86', roots: {}, adopted: {} });
// BIP-86: the output key commits to the internal key and no scripts.
export const tapkeyOf = pubkey => Tap.getPubKey(pubkey)[0];
const chainSecret = (keys, mnemonic, account) =>
(keys.scheme === 'wallet' ? walletSecret(mnemonic, account) : Tap.getSecKey(nostrSecret(mnemonic, account))[0]);
const pathAt = (keys, account) => (keys.scheme === 'wallet' ? PATHS.wallet(account) : PATHS.nostr(account));
function chainSecret(keys, mnemonic, account) {
if (keys.scheme === 'wallet') return walletSecret(mnemonic, account);
return Tap.getSecKey(keys.scheme === 'tapkey' ? nostrSecret(mnemonic, account) : walletSecret(mnemonic, account))[0];
}
const pathAt = (keys, account) => (keys.scheme === 'tapkey' ? PATHS.nostr(account) : PATHS.wallet(account));
export const rootWallet = (keys, mnemonic) => new TestWallet(chainSecret(keys, mnemonic, 0));
export const scriptHex = script => Script.encode(script, false).hex;
@@ -40,9 +42,22 @@ export function seedOf(keys, state, node) {
export function keyOf(keys, state, node) {
const seed = seedOf(keys, state, node);
if (!seed) return null;
const { npub, pubkey } = nostrKey(seed.mnemonic, seed.account);
return { wallet: new TestWallet(chainSecret(keys, seed.mnemonic, seed.account)), npub, pubkey, tapkey: keys.scheme === 'tapkey',
path: `${[seed.origin, ...seed.steps].join(' › ')} · ${pathAt(keys, seed.account)}` };
const { npub, pubkey } = nostrKey(seed.mnemonic, seed.account), from = [seed.origin, ...seed.steps].join(' › ');
return { wallet: new TestWallet(chainSecret(keys, seed.mnemonic, seed.account)), npub, pubkey, scheme: keys.scheme,
account: seed.account, from, path: `${from} · ${pathAt(keys, seed.account)}` };
}
// A node's public addresses, listed so no one has to derive them: Taproot holds its inscription and takes
// inscriptions, SegWit takes payments (this toy chain only moves Taproot), and the npub is its nostr name.
export function addressesOf(keys, state, node) {
const seed = seedOf(keys, state, node);
if (!seed) return null;
const k = keyOf(keys, state, node), segwit = segwitPubkey(seed.mnemonic, seed.account);
return {
taproot: { address: k.wallet.address, path: pathAt(keys, seed.account) },
segwit: { address: Address.p2wpkh.fromPubKey(segwit, 'regtest'), path: PATHS.segwit(seed.account) },
npub: { address: k.npub, path: PATHS.nostr(seed.account) },
};
}
// The wallet that holds an inscription now: the node's own key, its parent's (a handoff still cued), or the lab wallet.
+1 -1
View File
@@ -22,7 +22,7 @@ export function keysPreference(value) {
if (!/^[0-9a-f]{64}i0$/.test(id)) throw new Error('Invalid lab keys.');
keys[part][id] = assertMnemonic(mnemonic, 24);
}
keys.scheme = value?.scheme ?? (Object.keys(keys.roots).length ? 'wallet' : 'tapkey'); // v1.1 labs had roots and no scheme
keys.scheme = value?.scheme ?? (Object.keys(keys.roots).length ? 'wallet' : 'bip86'); // v1.1 labs had roots and no scheme
if (!SCHEMES.includes(keys.scheme)) throw new Error('Invalid lab keys.');
return keys;
}
+37 -18
View File
@@ -5,16 +5,16 @@ import { entropyToMnemonic } from '@scure/bip39';
import { wordlist } from '@scure/bip39/wordlists/english';
import { bytesToHex } from '@noble/hashes/utils';
import { schnorr } from '@noble/curves/secp256k1';
import { base64, bech32 } from '@scure/base';
import { Address } from '@cmdcode/tapscript';
import { PATHS, bip85Entropy, childMnemonic, nostrKey, nostrSecret, seedBytes, walletSecret } from '../src/adapters/lineage-keys.js';
import { base64 } from '@scure/base';
import { Address, Tap } from '@cmdcode/tapscript';
import { PATHS, bip85Entropy, childMnemonic, nostrKey, nostrSecret, seedBytes, segwitPubkey, walletSecret } from '../src/adapters/lineage-keys.js';
import { claimStatus, countersign, decodeClaim, holderCoin, makeClaim } from '../src/adapters/handoff.js';
import { LocalChain } from '../src/adapters/chain.js';
import { InscriptionAdapter } from '../src/adapters/inscription.js';
import { TestWallet } from '../src/adapters/wallet.js';
import { beat, decodeLab, emptyLab, encodeLab, keysPreference } from '../src/orgchain/store.js';
import { act, fund, lineageState } from '../src/orgchain/actions.js';
import { keyOf, leavesOf, rootWallet, scriptHex, tapkeyOf } from '../src/orgchain/keys.js';
import { addressesOf, keyOf, leavesOf, rootWallet, scriptHex, tapkeyOf } from '../src/orgchain/keys.js';
import { agentLeaves, agentRecords, nodeLeaves, nodeRecords, sshFingerprint } from '../src/orgchain/leaves.js';
// Spec vectors and the handoff test come from the tree branch (2ea7884), which wrote lineage-keys.js and handoff.js.
@@ -59,10 +59,14 @@ test('handoff: the recipient claims, the holder countersigns, the inscription mo
// Values read off the lineage key tree page (lineage.yolopreneur.lol) for this mnemonic: agent account 1, lineage.llc.
const LEADER = 'leader monkey parrot ring guide accident before fence cannon height naive bean';
test('tapkey: the BIP-86 tweak turns the spec internal key into the spec output key', () => {
const ABANDON = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
test('BIP-86 and BIP-84: the spec mnemonic gives the spec addresses', () => {
const tapkey = tapkeyOf('cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115');
assert.equal(tapkey, 'a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c');
assert.equal(Address.p2tr.fromPubKey(tapkey, 'main'), 'bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr');
const wallet = new TestWallet(Tap.getSecKey(walletSecret(ABANDON, 0, 0))[0]);
assert.equal(wallet.pubkey, tapkey);
assert.equal(Address.p2tr.fromPubKey(wallet.pubkey, 'main'), 'bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr');
assert.equal(Address.p2wpkh.fromPubKey(segwitPubkey(ABANDON, 0, 0), 'main'), 'bc1qcr8te4kr609gcawutmrza0j4xv80jy8z306fyu');
});
test('leaves match the lineage key tree page byte for byte', () => {
const agent = agentLeaves(nostrSecret(LEADER, 1));
@@ -81,22 +85,25 @@ test('leaves match the lineage key tree page byte for byte', () => {
});
const at = (lab, id) => scriptHex(holderCoin(lab.chain, id).scriptPubKey);
const key = (mnemonic, account) => `5120${tapkeyOf(nostrKey(mnemonic, account).pubkey)}`;
const npubHex = npub => bytesToHex(Uint8Array.from(bech32.fromWords(bech32.decode(npub).words)));
const key = (mnemonic, account) => `5120${new TestWallet(Tap.getSecKey(walletSecret(mnemonic, account))[0]).pubkey}`;
test('each holding has its own root, and every node holds its inscription at its tapkey', () => {
test('each holding has its own root, and every node holds its inscription at its Taproot address', () => {
const lab = emptyLab();
const root = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', root, { name: 'ops' });
const bot = act(lab, 'birth', ops, { name: 'bot', kind: 'agent' }), lead = act(lab, 'birth', root, { name: 'lead', kind: 'agent' });
const m = lab.keys.roots[root], opsSeed = childMnemonic(m, 0);
assert.equal(m.split(' ').length, 24);
assert.deepEqual([root, ops, bot, lead].map(id => at(lab, id)), [key(m, 0), key(opsSeed, 0), key(opsSeed, 1), key(m, 1)]);
const s = lineageState(lab), k = keyOf(lab.keys, s, s.nodes.get(bot));
assert.equal(k.path, "acme root › 0' · m/44'/1237'/1'/0/0");
assert.equal(k.npub, nostrKey(opsSeed, 1).npub);
assert.equal(k.wallet.pubkey, tapkeyOf(npubHex(k.npub)), 'the tapkey is the npub, tweaked: anyone with the npub finds the coins');
assert.equal(k.wallet.address, Address.p2tr.fromPubKey(k.wallet.pubkey, 'regtest'));
assert.ok(k.tapkey);
const s = lineageState(lab), k = keyOf(lab.keys, s, s.nodes.get(bot)), a = addressesOf(lab.keys, s, s.nodes.get(bot));
assert.equal(k.path, "acme root › 0' · m/86'/1'/1'/0/0");
assert.deepEqual([k.from, k.account, k.scheme], ["acme root › 0'", 1, 'bip86']);
assert.deepEqual(a, {
taproot: { address: Address.p2tr.fromPubKey(key(opsSeed, 1).slice(4), 'regtest'), path: "m/86'/1'/1'/0/0" },
segwit: { address: Address.p2wpkh.fromPubKey(segwitPubkey(opsSeed, 1), 'regtest'), path: "m/84'/1'/1'/0/0" },
npub: { address: nostrKey(opsSeed, 1).npub, path: "m/44'/1237'/1'/0/0" },
});
assert.match(a.taproot.address, /^bcrt1p/);
assert.match(a.segwit.address, /^bcrt1q/);
const award = act(lab, 'award', ops, { to: bot, skill: 'launch', belt: 'green' });
assert.equal(at(lab, award), key(opsSeed, 0), 'an act is signed by its authorizer, and its record stays with that key');
const other = act(lab, 'found', null, { name: 'other' });
@@ -132,11 +139,23 @@ test('graduation: a fresh root claims the org, the derived key countersigns, and
const decision = act(lab, 'decide', studio, { title: 'Our own root now' });
assert.equal(at(lab, decision), key(fresh, 0), 'the org now signs with its own root');
const s = lineageState(lab);
assert.equal(keyOf(lab.keys, s, s.nodes.get(seat)).path, "studio root · m/44'/1237'/1'/0/0");
assert.equal(keyOf(lab.keys, s, s.nodes.get(seat)).path, "studio root · m/86'/1'/1'/0/0");
assert.match(leavesOf(lab.keys, s, s.nodes.get(seat))[0][2], / seat@studio\.acme$/, 'names still nest after graduation');
assert.deepEqual(decodeLab(encodeLab(lab)).keys, lab.keys);
});
test('a lab keyed by v1.2 keeps its npub-tweaked chain keys', () => {
const lab = emptyLab();
lab.keys.scheme = 'tapkey';
const root = act(lab, 'found', null, { name: 'mid' }), seat = act(lab, 'birth', root, { name: 'seat', kind: 'agent' });
const m = lab.keys.roots[root];
assert.deepEqual([root, seat].map(id => at(lab, id)), [0, 1].map(a => `5120${tapkeyOf(nostrKey(m, a).pubkey)}`));
const s = lineageState(lab), k = keyOf(lab.keys, s, s.nodes.get(seat));
assert.equal(k.path, "mid root · m/44'/1237'/1'/0/0");
assert.equal(addressesOf(lab.keys, s, s.nodes.get(seat)).segwit.path, "m/84'/1'/1'/0/0", 'every lab lists SegWit');
assert.equal(decodeLab(encodeLab(lab)).keys.scheme, 'tapkey');
});
test('a lab keyed by v1.1 keeps its separate, untweaked chain keys', () => {
const lab = emptyLab();
lab.keys.scheme = 'wallet';
@@ -145,7 +164,7 @@ test('a lab keyed by v1.1 keeps its separate, untweaked chain keys', () => {
assert.deepEqual([root, seat].map(id => at(lab, id)), [0, 1].map(a => `5120${new TestWallet(walletSecret(m, a)).pubkey}`));
const s = lineageState(lab), k = keyOf(lab.keys, s, s.nodes.get(seat));
assert.equal(k.path, "old root · m/86'/1'/1'/0/0");
assert.equal(k.tapkey, false);
assert.equal(k.scheme, 'wallet');
const saved = JSON.parse(encodeLab(lab));
delete saved.keys.scheme; // v1.1 saved roots and no scheme
assert.equal(decodeLab(JSON.stringify(saved)).keys.scheme, 'wallet');
@@ -167,6 +186,6 @@ test('holdings from before lab keys still act, with the one lab wallet', () => {
assert.equal(s.nodes.get(kid).edge, 'adopted');
const saved = JSON.parse(encodeLab(lab));
delete saved.keys;
assert.deepEqual(decodeLab(JSON.stringify(saved)).keys, { scheme: 'tapkey', roots: {}, adopted: {} });
assert.deepEqual(decodeLab(JSON.stringify(saved)).keys, { scheme: 'bip86', roots: {}, adopted: {} });
assert.throws(() => keysPreference({ roots: { [`${'a'.repeat(64)}i0`]: 'not a mnemonic' } }), /Invalid lab mnemonic/);
});
+5 -5
View File
@@ -101,17 +101,17 @@ test('the sample replays, any beat can be inspected, and the lab survives a relo
await expect(page.locator('#inspect-body')).toContainText('"op": "graduate"');
await expect(page.locator('#inspect-body')).toContainText("well-known's key");
await row(page, 'lineage').click();
await expect(page.locator('#inspect-body')).toContainText("lineage root · m/44'/1237'/0'/0/0");
await expect(page.locator('#inspect-body')).toContainText('lineage root · account 0');
await expect(page.locator('#inspect-body')).toContainText("BIP-86 at m/86'/1'/0'/0/0");
await expect(page.locator('#inspect-body')).toContainText('its own key');
await row(page, 'time').click();
await expect(page.locator('#act-key')).toHaveText("Signs as time: yolopreneur root › 1' · m/44'/1237'/0'/0/0");
await expect(page.locator('#act-key')).toHaveText("Signs as time: yolopreneur root › 1' · m/86'/1'/0'/0/0");
const inspect = page.locator('#inspect-body');
for (const text of ['ssh-ca', 'lineage-ca@time.yolopreneur', 'mkey:']) await expect(inspect).toContainText(text);
await row(page, 'sol').click();
const fact = name => inspect.locator('.f').filter({ has: page.locator('dt', { hasText: new RegExp(`^${name}$`) }) });
await expect(fact('Tapkey')).toContainText('bcrt1p');
await expect(fact('npub')).toContainText('npub1');
await expect(fact('Derived')).toContainText("yolopreneur root › 1' · m/44'/1237'/2'/0/0");
for (const [name, text] of [['Taproot', 'bcrt1p'], ['Taproot', "BIP-86 at m/86'/1'/2'/0/0"], ['SegWit', 'bcrt1q'], ['SegWit', "BIP-84 at m/84'/1'/2'/0/0"],
['npub', 'npub1'], ['npub', "NIP-06 at m/44'/1237'/2'/0/0"], ['From', "yolopreneur root › 1' · account 2"]]) await expect(fact(name)).toContainText(text);
for (const text of ['ssh-ed25519 AAAAC3NzaC1lZDI1NTE5', 'sol@time.yolopreneur namespaces="git"', 'sol._domainkey.time.yolopreneur. IN TXT "v=DKIM1; k=ed25519; p=', 'PublicKey = ']) {
await expect(inspect.locator('pre')).toContainText(text);
}