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' ;
import { bytesToHex } from '@noble/hashes/utils' ;
import { schnorr } from '@noble/curves/secp256k1' ;
2026-10-14 19:09:50 -07:00
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' ;
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' ;
import { act , fund , lineageState } from '../src/orgchain/actions.js' ;
2026-10-14 19:09:50 -07:00
import { keyOf , leavesOf , rootWallet , scriptHex , tapkeyOf } from '../src/orgchain/keys.js' ;
import { agentLeaves , agentRecords , nodeLeaves , nodeRecords , 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' ;
test ( 'tapkey: the BIP-86 tweak turns the spec internal key into the spec output key' , () => {
const tapkey = tapkeyOf ( 'cc8a4bc64d897bddc5fbc2f670f7a8ba0b386779106cf1223c6fc5d7cd6fc115' );
assert . equal ( tapkey , 'a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c' );
assert . equal ( Address . p2tr . fromPubKey ( tapkey , 'main' ), 'bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr' );
});
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 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 )));
test ( 'each holding has its own root, and every node holds its inscription at its tapkey' , () => {
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' });
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 ));
2026-10-14 19:09:50 -07:00
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 );
2026-10-14 18:22:22 -07:00
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 );
});
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' });
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 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' });
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 )]);
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:09:50 -07:00
assert . equal ( keyOf ( lab . keys , s , s . nodes . get ( seat )). path , "studio root · m/44'/1237'/1'/0/0" );
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-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" );
assert . equal ( k . tapkey , false );
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:09:50 -07:00
assert . deepEqual ( decodeLab ( JSON . stringify ( saved )). keys , { scheme : 'tapkey' , roots : {}, adopted : {} });
2026-10-14 18:22:22 -07:00
assert . throws (() => keysPreference ({ roots : { [ ` ${ 'a' . repeat ( 64 ) } i0` ] : 'not a mnemonic' } }), /Invalid lab mnemonic/ );
});