Orgchain v1.3: Taproot and SegWit addresses for every node
Every account now lists standard Bitcoin 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, and SegWit (BIP-84, m/84'/1'/account'/0/0) for payments, beside its npub (NIP-06). An org is account 0 of its seed and its agents are accounts 1..n, so a wallet that opens an account finds the same addresses. The lineage key tree page derives the same branches on mainnet paths and lists the addresses in each agent's keyring. The v1.2 tapkey (the npub's key, tweaked) is replaced by the standard BIP-86 key; labs keyed by v1.1 or v1.2 keep their chain keys until they are reset. Tests pin BIP-86 and BIP-84's own vectors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4upF5LVwmPDDqrhdUfnXh
This commit is contained in:
1 parent
00a35613c5
commit
0af51dc9cd
8 files changed
+107
-62
No files matched your search
@@ -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`.
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+12
-8
@@ -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 })))),
|
||||
|
||||
+32
-17
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+37
-18
@@ -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/);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user