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
+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