2026-10-14 18:22:22 -07:00
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' ;
2026-10-14 20:14:11 -07:00
import { bytesToHex , utf8ToBytes } from '@noble/hashes/utils' ;
import { sha256 } from '@noble/hashes/sha2' ;
2026-10-14 18:22:22 -07:00
import { schnorr } from '@noble/curves/secp256k1' ;
2026-10-14 19:49:05 -07:00
import { base64 } from '@scure/base' ;
import { Address , Tap } from '@cmdcode/tapscript' ;
2026-10-15 10:48:28 -07:00
import { PATHS , bip85Entropy , childMnemonic , ethAddress , ethSecret , nostrKey , nostrSecret , seedBytes , segwitPubkey , segwitSecret , walletSecret } from '../src/adapters/lineage-keys.js' ;
2026-10-14 18:22:22 -07:00
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' ;
2026-10-14 23:16:03 -07:00
import { act , fund , lineageState , send } from '../src/orgchain/actions.js' ;
import { SAT } from '../src/orgchain/money.js' ;
2026-10-14 20:14:11 -07:00
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' ;
2026-10-14 18:22:22 -07:00
// 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/ );
});
2026-10-14 19:09:50 -07:00
// 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' ;
2026-10-14 19:49:05 -07:00
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' , () => {
2026-10-14 19:09:50 -07:00
const tapkey = tapkeyOf ( 'cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115' );
assert . equal ( tapkey , 'a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c' );
2026-10-14 19:49:05 -07:00
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' );
2026-10-14 19:09:50 -07:00
});
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=' );
});
2026-10-14 20:14:11 -07:00
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$/ );
});
2026-10-14 19:09:50 -07:00
const at = ( lab , id ) => scriptHex ( holderCoin ( lab . chain , id ). scriptPubKey );
2026-10-14 19:49:05 -07:00
const key = ( mnemonic , account ) => `5120 ${ new TestWallet ( Tap . getSecKey ( walletSecret ( mnemonic , account ))[ 0 ]). pubkey } ` ;
2026-10-14 19:09:50 -07:00
2026-10-14 19:49:05 -07:00
test ( 'each holding has its own root, and every node holds its inscription at its Taproot address' , () => {
2026-10-14 18:22:22 -07:00
const lab = emptyLab ();
const root = act ( lab , 'found' , null , { name : 'acme' }), ops = act ( lab , 'birth' , root , { name : 'ops' });
2026-10-14 23:16:03 -07:00
send ( lab , root , ops , SAT ); // every act pays its own gas
2026-10-14 18:22:22 -07:00
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 )]);
2026-10-14 19:49:05 -07:00
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" },
2026-10-14 20:14:11 -07:00
tapkey : { address : Address . p2tr . fromPubKey ( tapkeyOf ( nostrKey ( opsSeed , 1 ). pubkey ), 'regtest' ), path : "m/44'/1237'/1'/0/0" },
2026-10-14 19:49:05 -07:00
npub : { address : nostrKey ( opsSeed , 1 ). npub , path : "m/44'/1237'/1'/0/0" },
2026-10-15 10:48:28 -07:00
ethereum : { address : ethAddress ( opsSeed , 1 ), path : "m/44'/60'/1'/0/0" },
2026-10-14 19:49:05 -07:00
});
assert . match ( a . taproot . address , /^bcrt1p/ );
assert . match ( a . segwit . address , /^bcrt1q/ );
2026-10-15 15:48:31 -07:00
// 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 );
2026-10-14 18:22:22 -07:00
const other = act ( lab , 'found' , null , { name : 'other' });
assert . notEqual ( lab . keys . roots [ other ], m );
});
2026-10-14 19:09:50 -07:00
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' });
2026-10-14 23:16:03 -07:00
send ( lab , root , ops , SAT );
2026-10-14 19:09:50 -07:00
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$/ );
});
2026-10-14 20:14:11 -07:00
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' });
2026-10-14 23:16:03 -07:00
send ( lab , root , ops , SAT );
2026-10-14 20:14:11 -07:00
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 );
2026-10-15 10:48:28 -07:00
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 )) } )` ),
2026-10-14 20:14:11 -07:00
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' );
});
2026-10-14 18:22:22 -07:00
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' });
2026-10-14 23:16:03 -07:00
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' });
2026-10-14 18:22:22 -07:00
const seat = act ( lab , 'birth' , studio , { name : 'seat' , kind : 'agent' }), solo = act ( lab , 'birth' , studio , { name : 'solo' });
2026-10-14 23:16:03 -07:00
send ( lab , studio , solo , 0.1 * SAT ); // a graduate pays for the claim on its own root
2026-10-14 18:22:22 -07:00
const old = childMnemonic ( lab . keys . roots [ root ], 0 );
2026-10-15 14:51:52 -07:00
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 })];
2026-10-14 18:22:22 -07:00
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 )]);
2026-10-14 19:09:50 -07:00
const leftovers = lab . chain . coins ( rootWallet ( lab . keys , old ). script ). flatMap ( c => ( c . inscriptions ?? []). map ( i => i . id ));
2026-10-14 18:22:22 -07:00
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 );
2026-10-14 19:49:05 -07:00
assert . equal ( keyOf ( lab . keys , s , s . nodes . get ( seat )). path , "studio root · m/86'/1'/1'/0/0" );
2026-10-14 19:09:50 -07:00
assert . match ( leavesOf ( lab . keys , s , s . nodes . get ( seat ))[ 0 ][ 2 ], / seat@studio\.acme$/ , 'names still nest after graduation' );
2026-10-14 18:22:22 -07:00
assert . deepEqual ( decodeLab ( encodeLab ( lab )). keys , lab . keys );
2026-10-15 14:51:52 -07:00
// 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 );
2026-10-14 18:22:22 -07:00
});
2026-10-14 19:49:05 -07:00
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' );
2026-10-14 20:14:11 -07:00
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' );
2026-10-14 19:49:05 -07:00
assert . equal ( decodeLab ( encodeLab ( lab )). keys . scheme , 'tapkey' );
});
2026-10-14 19:09:50 -07:00
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" );
2026-10-14 19:49:05 -07:00
assert . equal ( k . scheme , 'wallet' );
2026-10-14 20:14:11 -07:00
assert . match ( secretsOf ( lab . keys , s , s . nodes . get ( seat )). account [ 1 ][ 2 ], /^rawtr\(c/ , 'an untweaked key imports as rawtr()' );
2026-10-14 19:09:50 -07:00
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/ );
});
2026-10-14 18:22:22 -07:00
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 );
2026-10-14 19:09:50 -07:00
assert . equal ( leavesOf ( lab . keys , s , s . nodes . get ( kid )), null );
2026-10-14 18:22:22 -07:00
assert . equal ( s . nodes . get ( kid ). edge , 'adopted' );
const saved = JSON . parse ( encodeLab ( lab ));
delete saved . keys ;
2026-10-14 19:49:05 -07:00
assert . deepEqual ( decodeLab ( JSON . stringify ( saved )). keys , { scheme : 'bip86' , roots : {}, adopted : {} });
2026-10-14 18:22:22 -07:00
assert . throws (() => keysPreference ({ roots : { [ ` ${ 'a' . repeat ( 64 ) } i0` ] : 'not a mnemonic' } }), /Invalid lab mnemonic/ );
});