diff --git a/AGENTS.md b/AGENTS.md index 0ed0004..e1ae0f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`. diff --git a/README.md b/README.md index 2260c42..9dea163 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/adapters/lineage-keys.js b/src/adapters/lineage-keys.js index 4758214..e287678 100644 --- a/src/adapters/lineage-keys.js +++ b/src/adapters/lineage-keys.js @@ -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); } diff --git a/src/main.js b/src/main.js index 56bdd8e..7912c65 100644 --- a/src/main.js +++ b/src/main.js @@ -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 })))), diff --git a/src/orgchain/keys.js b/src/orgchain/keys.js index b1df61c..05408a0 100644 --- a/src/orgchain/keys.js +++ b/src/orgchain/keys.js @@ -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. diff --git a/src/orgchain/store.js b/src/orgchain/store.js index 408994f..58ec51f 100644 --- a/src/orgchain/store.js +++ b/src/orgchain/store.js @@ -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; } diff --git a/tests/keys.test.js b/tests/keys.test.js index cb43015..753845d 100644 --- a/tests/keys.test.js +++ b/tests/keys.test.js @@ -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/); }); diff --git a/tests/orgchain.spec.js b/tests/orgchain.spec.js index 5b4d80b..f6c5216 100644 --- a/tests/orgchain.spec.js +++ b/tests/orgchain.spec.js @@ -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); }