import test from 'node:test'; import assert from 'node:assert/strict'; import { HDKey } from '@scure/bip32'; import { entropyToMnemonic } from '@scure/bip39'; import { wordlist } from '@scure/bip39/wordlists/english'; import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils'; import { sha256 } from '@noble/hashes/sha2'; import { schnorr } from '@noble/curves/secp256k1'; import { base64 } from '@scure/base'; import { Address, Tap } from '@cmdcode/tapscript'; import { PATHS, bip85Entropy, childMnemonic, ethAddress, ethSecret, nostrKey, nostrSecret, seedBytes, segwitPubkey, segwitSecret, 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, send } from '../src/orgchain/actions.js'; import { SAT } from '../src/orgchain/money.js'; import { addressesOf, descsum, keyOf, leavesOf, nsecOf, rootWallet, scriptHex, secretsOf, tapkeyOf, wif } from '../src/orgchain/keys.js'; import { agentLeaves, agentRecords, agentSecrets, nodeLeaves, nodeRecords, nodeSecrets, 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. const XPRV = 'xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb'; test('BIP-85 spec vectors: core entropy and the 24-word BIP-39 application', () => { const root = HDKey.fromExtendedKey(XPRV); assert.equal(bytesToHex(root.derive("m/83696968'/0'/0'").privateKey), 'cca20ccb0e9a90feb0912870c3323b24874b0ca3d8018c4b96d0b97c0e82ded0'); assert.equal(bytesToHex(bip85Entropy(root, "m/83696968'/0'/0'")), 'efecfbccffea313214232d29e71563d941229afb4338c21f9517c41aaa0d16f00b83d2a09ef747e7a64e8e2bd5a14869e693da66ce94ac2da570ab7ee48618f7'); const entropy = bip85Entropy(root, PATHS.orgSeed(0)).slice(0, 32); assert.equal(bytesToHex(entropy), 'ae131e2312cdc61331542efe0d1077bac5ea803adf24b313a4f0e48e9c51f37f'); assert.equal(entropyToMnemonic(entropy, wordlist), 'puppy ocean match cereal symbol another shed magic wrap hammer bulb intact gadget divorce twin tonight reason outdoor destroy simple truth cigar social volcano'); }); test('BIP-86 and NIP-06 spec vectors', () => { const abandon = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; assert.equal(bytesToHex(schnorr.getPublicKey(walletSecret(abandon, 0, 0))), 'cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115'); assert.deepEqual(nostrKey('leader monkey parrot ring guide accident before fence cannon height naive bean', 0), { pubkey: '17162c921dc4d2518f9a101db33695df1afb56ab82f5ff3e5da6eec3ca5cd917', npub: 'npub1zutzeysacnf9rru6zqwmxd54mud0k44tst6l70ja5mhv8jjumytsd2x7nu' }); }); test('handoff: the recipient claims, the holder countersigns, the inscription moves', () => { const chain = new LocalChain(), holder = TestWallet.create(), own = TestWallet.create(); chain.mine(holder.script); const ins = new InscriptionAdapter(chain, holder); const top = ins.inscribe('time', null, { op: 'found', name: 'time' }, { metaprotocol: 'lineage/v0' }); const doc = ins.inscribe('docscope', top, { op: 'birth', name: 'docscope' }, { metaprotocol: 'lineage/v0' }); chain.mine(holder.script); assert.throws(() => makeClaim(chain, doc, own), /plain coin/); chain.mine(own.script); assert.throws(() => makeClaim(chain, doc, holder), /already holds/); const claim = makeClaim(chain, doc, own); assert.equal(claimStatus(chain, claim), 'open'); assert.deepEqual(decodeClaim(JSON.parse(JSON.stringify(claim))), claim); assert.throws(() => countersign(chain, claim, own), /current holder/); assert.throws(() => countersign(chain, { ...claim, to: holder.pubkey }, holder), /signature/i, 'a claim edited after signing fails'); assert.equal(countersign(chain, claim, holder), claim.txid); chain.mine(holder.script); assert.ok(new InscriptionAdapter(chain, own).list().some(i => i.id === doc && i.parent === top)); assert.ok(new InscriptionAdapter(chain, holder).list().some(i => i.id === top), 'the parent keeps its own inscription'); assert.equal(claimStatus(chain, claim), 'stale'); assert.throws(() => countersign(chain, claim, holder), /spent/); }); // 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'; 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'); 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)); assert.equal(nostrKey(LEADER, 1).npub, 'npub1m9m6dnc0svwugus8sz6l29rqatmdegywxtgld6ymvq6y6ca0fczq88tsjr'); assert.deepEqual([sshFingerprint(agent.ssh), sshFingerprint(agent.git), base64.encode(agent.dkim), base64.encode(agent.wg)], [ 'SHA256:Svt6sRSfHEYSQf9+QX+XDPg9eXlEsgcy+OAcJIzeyQU', 'SHA256:fBH0g7huqXVWQHa6GiDsMMzg2PTnWwyTgJdrH3L5bo4', 'A7j292wHd4jUXhs4dMJcKh0nDG1/cwoo4aG1NUn/x2g=', '0HVH6534UBMEVWDz8BA+NkI73Z28bHBBQNFEONCkmWI=']); assert.deepEqual(nodeRecords(nodeLeaves(seedBytes(LEADER)), 'lineage.llc').map(([, value]) => value), [ 'SHA256:XOvpZ24mpcsFhi0mDxyZDqSEpkVKsQkhnLCOR5mlxY4', 'ObnNHsWk0TI5evUH+wbuMXtyPtEXInA6Y0KJbFu8S7A=', 'mkey:e35077cb9d3e78ad67898407caa1a059453fcec698f00f3585c6d87e7950e676']); const lines = Object.fromEntries(agentRecords(agent, 'sol@lineage.llc', 'sol', 'lineage.llc').map(([label, , line]) => [label, line])); assert.match(lines.ssh, /^ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI[A-Za-z0-9+/]{43} sol@lineage\.llc$/); assert.match(lines.git, /^sol@lineage\.llc namespaces="git" ssh-ed25519 \S+ sol@lineage\.llc$/); assert.equal(lines.dkim, 'sol._domainkey.lineage.llc. IN TXT "v=DKIM1; k=ed25519; p=A7j292wHd4jUXhs4dMJcKh0nDG1/cwoo4aG1NUn/x2g="'); assert.equal(lines.wg, '[Peer]\n# sol@lineage.llc\nPublicKey = 0HVH6534UBMEVWDz8BA+NkI73Z28bHBBQNFEONCkmWI='); }); const sha16 = text => bytesToHex(sha256(utf8ToBytes(text))).slice(0, 16); test('secrets match the lineage key tree page byte for byte', () => { const page = { ssh: '39b2f5573e3bce0b', git: 'dc59220f725526ac', dkim: 'd3bd4ee4fab19a5e', wg: 'def16367a9dcc19a', 'ssh-ca': 'ac0e7b5448ba3818', mtls: '9ef175861cefe59d', noise: 'bea4ba0c87cc7707' }; const ours = [...agentSecrets(nostrSecret(LEADER, 1), 'agent-a@lineage.llc'), ...nodeSecrets(seedBytes(LEADER), 'lineage.llc')]; assert.deepEqual(Object.fromEntries(ours.map(([label, , text]) => [label, sha16(text)])), page); // The page shows mainnet forms (WIF version 0x80); the lab's own descriptors use test networks. assert.deepEqual([nsecOf(nostrSecret(LEADER, 1)), descsum(`tr(${wif(nostrSecret(LEADER, 1), 0x80)})`), descsum(`tr(${wif(walletSecret(LEADER, 1, 0), 0x80)})`), descsum(`wpkh(${wif(segwitSecret(LEADER, 1, 0), 0x80)})`)].map(sha16), ['8d07c2e20021a973', '4b4d32db9ac69c8d', '1908b363e70a2120', '5caff55640aca16d']); assert.equal(wif(segwitSecret(ABANDON, 0, 0), 0x80), 'KyZpNDKnfs94vbrwhJneDi77V6jF64PWPF8x5cdJb8ifgg2DUc9d', 'BIP-84 vector'); assert.equal(descsum('raw(deadbeef)'), 'raw(deadbeef)#89f8spxm', 'BIP-380 vector'); assert.match(ours[0][2], /^-----BEGIN OPENSSH PRIVATE KEY-----\n[A-Za-z0-9+/=\n]+\n-----END OPENSSH PRIVATE KEY-----\n$/); }); const at = (lab, id) => scriptHex(holderCoin(lab.chain, id).scriptPubKey); 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 Taproot address', () => { const lab = emptyLab(); const root = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', root, { name: 'ops' }); send(lab, root, ops, SAT); // every act pays its own gas 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)), 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" }, tapkey: { address: Address.p2tr.fromPubKey(tapkeyOf(nostrKey(opsSeed, 1).pubkey), 'regtest'), path: "m/44'/1237'/1'/0/0" }, npub: { address: nostrKey(opsSeed, 1).npub, path: "m/44'/1237'/1'/0/0" }, ethereum: { address: ethAddress(opsSeed, 1), path: "m/44'/60'/1'/0/0" }, }); assert.match(a.taproot.address, /^bcrt1p/); assert.match(a.segwit.address, /^bcrt1q/); // A belt is signed for by its authorizer (ops, its parent) and rides its recipient's sat: one coin, one sat, at bot's key. const award = act(lab, 'award', ops, { to: bot, skill: 'launch', belt: 'green' }), coin = holderCoin(lab.chain, award); assert.equal(at(lab, award), key(opsSeed, 1), "a belt rides its recipient's sat"); assert.equal(coin, holderCoin(lab.chain, bot)); assert.equal(new Set(coin.inscriptions.map(i => i.sat)).size, 1); const other = act(lab, 'found', null, { name: 'other' }); assert.notEqual(lab.keys.roots[other], m); }); test('leaves hang off the same tree: agents from their nostr secret, orgs from their seed', () => { const lab = emptyLab(); const root = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', root, { name: 'ops' }); send(lab, root, ops, SAT); const bot = act(lab, 'birth', ops, { name: 'bot', kind: 'agent' }); const opsSeed = childMnemonic(lab.keys.roots[root], 0), s = lineageState(lab), node = id => s.nodes.get(id); assert.deepEqual(leavesOf(lab.keys, s, node(bot)), agentRecords(agentLeaves(nostrSecret(opsSeed, 1)), 'bot@ops.acme', 'bot', 'ops.acme')); assert.deepEqual(leavesOf(lab.keys, s, node(ops)), nodeRecords(nodeLeaves(seedBytes(opsSeed)), 'ops.acme')); assert.deepEqual(leavesOf(lab.keys, s, node(root)).map(([label]) => label), ['ssh-ca', 'mtls', 'noise']); assert.match(leavesOf(lab.keys, s, node(root))[0][2], / lineage-ca@acme$/); }); test('secrets: an org shows its seed, a seat only its account, and every descriptor imports as it is', () => { const lab = emptyLab(); const root = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', root, { name: 'ops' }); send(lab, root, ops, SAT); const bot = act(lab, 'birth', ops, { name: 'bot', kind: 'agent' }); const m = lab.keys.roots[root], opsSeed = childMnemonic(m, 0), s = lineageState(lab), node = id => s.nodes.get(id); assert.deepEqual(secretsOf(lab.keys, s, node(root)).seed, { from: 'acme root', mnemonic: m }); assert.deepEqual(secretsOf(lab.keys, s, node(ops)).seed, { from: "acme root › 0'", mnemonic: opsSeed }); const seat = secretsOf(lab.keys, s, node(bot)); assert.equal(seat.seed, null); assert.deepEqual(seat.account.map(([label]) => label), ['nsec', 'taproot', 'segwit', 'tapkey', 'ethereum']); assert.equal(seat.account[4][2], `0x${ethSecret(opsSeed, 1)}`, 'as the key tree page shows it'); assert.deepEqual(seat.account.slice(1, 4).map(([, , d]) => d), [descsum(`tr(${wif(walletSecret(opsSeed, 1))})`), descsum(`wpkh(${wif(segwitSecret(opsSeed, 1))})`), descsum(`tr(${wif(nostrSecret(opsSeed, 1))})`)]); assert.match(seat.account[1][2], /^tr\(c[1-9A-HJ-NP-Za-km-z]{51}\)#[a-z0-9]{8}$/, 'test-network WIF'); assert.deepEqual(seat.leaves, agentSecrets(nostrSecret(opsSeed, 1), 'bot@ops.acme')); assert.deepEqual(secretsOf(lab.keys, s, node(ops)).leaves, nodeSecrets(seedBytes(opsSeed), 'ops.acme')); assert.equal(addressesOf(lab.keys, s, node(bot)).tapkey.address, Address.p2tr.fromPubKey(tapkeyOf(nostrKey(opsSeed, 1).pubkey), 'regtest'), 'the npub alone finds the tapkey'); }); test('graduation: a fresh root claims the org, the derived key countersigns, and the subtree re-keys', () => { const lab = emptyLab(); const root = act(lab, 'found', null, { name: 'acme' }), studio = act(lab, 'birth', root, { name: 'studio' }); send(lab, root, studio, 2 * SAT); const kid = act(lab, 'birth', studio, { name: 'kid' }); send(lab, studio, kid, SAT); const deep = act(lab, 'birth', kid, { name: 'deep' }); const seat = act(lab, 'birth', studio, { name: 'seat', kind: 'agent' }), solo = act(lab, 'birth', studio, { name: 'solo' }); send(lab, studio, solo, 0.1 * SAT); // a graduate pays for the claim on its own root const old = childMnemonic(lab.keys.roots[root], 0); const freed = act(lab, 'graduate', studio, { of: solo }); // solo graduates first and keeps its own root through what follows const soloRoot = lab.keys.adopted[freed]; // fresh roots are kept by graduation, since a node can graduate more than once const fresh = lab.keys.adopted[act(lab, 'graduate', root, { of: studio })]; assert.ok(fresh && fresh !== old); assert.deepEqual([studio, kid, deep, seat, solo].map(id => at(lab, id)), [key(fresh, 0), key(childMnemonic(fresh, 0), 0), key(childMnemonic(childMnemonic(fresh, 0), 0), 0), key(fresh, 1), key(soloRoot, 0)]); const leftovers = lab.chain.coins(rootWallet(lab.keys, old).script).flatMap(c => (c.inscriptions ?? []).map(i => i.id)); assert.deepEqual(leftovers.filter(id => [studio, kid, deep, seat].includes(id)), []); 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/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); // Labs before v1.13 kept a graduate's root under its own id; they still find it. const address = () => keyOf(lab.keys, lineageState(lab), lineageState(lab).nodes.get(solo)).wallet.address, before = address(); lab.keys.adopted[solo] = soloRoot; delete lab.keys.adopted[freed]; assert.equal(address(), before); }); 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'); const v12 = secretsOf(lab.keys, s, s.nodes.get(seat)).account; assert.equal(v12[1][2], v12[3][2], 'in a v1.2 lab the Taproot key is the tapkey'); 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'; const root = act(lab, 'found', null, { name: 'old' }), 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${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.scheme, 'wallet'); assert.match(secretsOf(lab.keys, s, s.nodes.get(seat)).account[1][2], /^rawtr\(c/, 'an untweaked key imports as rawtr()'); 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'); assert.throws(() => keysPreference({ scheme: 'nope' }), /Invalid lab keys/); }); test('holdings from before lab keys still act, with the one lab wallet', () => { const lab = emptyLab(); fund(lab); const legacy = new InscriptionAdapter(lab.chain, lab.wallet).inscribe('found old', null, { op: 'found', name: 'old' }, { metaprotocol: 'lineage/v0' }); beat(lab); const kid = act(lab, 'birth', legacy, { name: 'kid' }); assert.equal(at(lab, kid), `5120${lab.wallet.pubkey}`); act(lab, 'graduate', legacy, { of: kid }); assert.equal(at(lab, kid), `5120${lab.wallet.pubkey}`, 'without a root there is nothing to re-key; the event is still recorded'); const s = lineageState(lab); assert.equal(keyOf(lab.keys, s, s.nodes.get(kid)), null); assert.equal(leavesOf(lab.keys, s, s.nodes.get(kid)), null); 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: 'bip86', roots: {}, adopted: {} }); assert.throws(() => keysPreference({ roots: { [`${'a'.repeat(64)}i0`]: 'not a mnemonic' } }), /Invalid lab mnemonic/); });