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 } from '@noble/hashes/utils'; import { schnorr } from '@noble/curves/secp256k1'; import { PATHS, bip85Entropy, childMnemonic, nostrKey, 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, scriptHex } from '../src/orgchain/keys.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/); }); const at = (lab, id) => scriptHex(holderCoin(lab.chain, id).scriptPubKey); const key = (mnemonic, account) => `5120${new TestWallet(walletSecret(mnemonic, account)).pubkey}`; test('each holding has its own root, and every node holds its inscription at its lineage key', () => { 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/86'/1'/1'/0/0"); assert.match(k.npub, /^npub1[02-9ac-hj-np-z]{58}$/); 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' }); assert.notEqual(lab.keys.roots[other], m); }); 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' }); const kid = act(lab, 'birth', studio, { name: 'kid' }), deep = act(lab, 'birth', kid, { name: 'deep' }); const seat = act(lab, 'birth', studio, { name: 'seat', kind: 'agent' }), solo = act(lab, 'birth', studio, { name: 'solo' }); const old = childMnemonic(lab.keys.roots[root], 0); act(lab, 'graduate', studio, { of: solo }); // solo graduates first and keeps its own root through what follows const soloRoot = lab.keys.adopted[solo]; act(lab, 'graduate', root, { of: studio }); const fresh = lab.keys.adopted[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(new TestWallet(walletSecret(old, 0)).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.deepEqual(decodeLab(encodeLab(lab)).keys, 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(s.nodes.get(kid).edge, 'adopted'); const saved = JSON.parse(encodeLab(lab)); delete saved.keys; assert.deepEqual(decodeLab(JSON.stringify(saved)).keys, { roots: {}, adopted: {} }); assert.throws(() => keysPreference({ roots: { [`${'a'.repeat(64)}i0`]: 'not a mnemonic' } }), /Invalid lab mnemonic/); });