Files
teahubxyzandClaude Opus 5.5 6998ae47aa Orgchain v1.10: keyrings, each key signing in its own chain's standard
A new lineage/v0 op binds a node's keys in one inscription whose parent is the
node itself: its Taproot and SegWit addresses, its npub and its Ethereum
address. Every key signs the same statement the way its own wallets sign a
message: BIP-322 simple signatures for the Bitcoin addresses, a NIP-01 note
(kind 1, created_at 0) for the npub, EIP-191 for Ethereum. The reducer checks
every proof, binds a standing keyring's keys to its node, lets the newest
keyring replace the older, and retires keyrings at a graduation (the keys
change with the new root) and at a close.

Every account gets an Ethereum key at m/44'/60'/account'/0/0; an org's, at
account 0, is the key tree page's legal-control key and matches its published
vector. Keyring is the ninth verb; Inspect shows each node's keyring and the
statement its keys signed, and Reveal its Ethereum private key. The sample
publishes three keyrings, and lineage's retires when it graduates.

Tests pin BIP-322's published vectors and values cross-checked with
bip322-js, ethers and nostr-tools. The regtest rehearsal, rerun with five
keyrings, confirmed 89 transactions on Bitcoin Core; ord's 26 events fold to
the same trees and keyrings, every proof checked from ord's copy, and the
fixture is re-recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4upF5LVwmPDDqrhdUfnXh
2026-10-15 10:48:28 -07:00

171 lines
13 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { bytesToHex } from '@noble/hashes/utils';
import { sha256 } from '@noble/hashes/sha2';
import { ripemd160 } from '@noble/hashes/legacy';
import { schnorr, secp256k1 } from '@noble/curves/secp256k1';
import { bech32, bech32m, createBase58check } from '@scure/base';
import { Tap } from '@cmdcode/tapscript';
import { KEYS, decodeAddress, keyringWhy, messageHash, signBip322, signEip191, signKeyring, signNostr, slots, statement, toSpendTxid,
verifyBip322, verifyEip191, verifyNostr } from '../src/orgchain/keyring.js';
import { childMnemonic, ethAddress, ethAddressOf, ethSecret } from '../src/adapters/lineage-keys.js';
import { PROTOCOL, reduce, summary } from '../src/orgchain/lineage.js';
import { emptyLab, setTempo } from '../src/orgchain/store.js';
import { act, lineageState, send } from '../src/orgchain/actions.js';
import { ringOf } from '../src/orgchain/keys.js';
import { SAT } from '../src/orgchain/money.js';
const utf8 = text => new TextEncoder().encode(text);
// BIP-322's test vectors (bip-0322.mediawiki): one key, as a P2WPKH address and with the BIP-86 tweak as Taproot.
const VECTOR = { wif: 'L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k', segwit: 'bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l',
taproot: 'bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3' };
const vectorSecret = bytesToHex(createBase58check(sha256).decode(VECTOR.wif).slice(1, 33));
const toSpend = (address, message) => bytesToHex(toSpendTxid(decodeAddress(address), message).reverse());
test('BIP-322: message hashes, to_spend and signatures match the BIP', () => {
assert.equal(bytesToHex(messageHash('')), 'c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1');
assert.equal(bytesToHex(messageHash('Hello World')), 'f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a');
assert.equal(toSpend(VECTOR.segwit, ''), 'c5680aa69bb8d860bf82d4e9cd3504b55dde018de765a91bb566283c545a99a7');
assert.equal(toSpend(VECTOR.segwit, 'Hello World'), 'b79d196740ad5217771c1098fc4a4b51e0535c32236c71f1ea4d61a2d603352b');
const empty = 'AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=';
const hello = 'AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=';
const taproot = 'AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==';
assert.ok(verifyBip322(VECTOR.segwit, '', empty));
assert.ok(verifyBip322(VECTOR.segwit, 'Hello World', hello));
assert.ok(verifyBip322(VECTOR.taproot, 'Hello World', taproot), 'a 65-byte Taproot signature carries SIGHASH_ALL');
assert.ok(!verifyBip322(VECTOR.segwit, 'Hello World', empty));
assert.ok(!verifyBip322(VECTOR.taproot, 'Hello world', taproot));
// RFC 6979 without low-R grinding gives the BIP's second "Hello World" signature, byte for byte.
assert.equal(signBip322(VECTOR.segwit, 'Hello World', vectorSecret),
'AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy');
// A Taproot signature with zero aux randomness (SIGHASH_DEFAULT, 64 bytes); bip322-js 3.0.0 accepts it.
const mine = signBip322(VECTOR.taproot, 'Hello World', Tap.getSecKey(vectorSecret)[0]);
assert.equal(mine, 'AUDjpClYFHngjnqQ3F0/3dyrLsOHFNEm4rKaaAc9GsfhC5+DngPJmXTeAmz+yfsVRa61PD2k9/CEQnLDvNUn9Qug');
assert.ok(verifyBip322(VECTOR.taproot, 'Hello World', mine));
for (const junk of ['', 'AA==', 'not base64', mine.slice(0, -8), null]) assert.ok(!verifyBip322(VECTOR.taproot, 'Hello World', junk));
assert.ok(!verifyBip322(VECTOR.taproot.toUpperCase(), 'Hello World', mine), 'keyring addresses are lowercase');
});
test('Ethereum: addresses and EIP-191 signatures match ethers and the key tree page', () => {
const m = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about';
// Account 0 is the key tree page's legal-control key, its published vector; ethers 6 derives the same for account 1.
assert.equal(ethAddress(m, 0), '0x9858EfFD232B4033E47d90003D41EC34EcaEda94');
assert.equal(ethAddress(m, 1), '0x78839F6054d7ed13918bAe0473BA31b1Ca9D7265');
const sig = signEip191('lineage/v0 keyring', ethSecret(m, 0));
assert.equal(sig, '0x9548689d750b83e33f4c07dd68dbc620f4733ac43fc623bebaef530d0f0dd055559d1a619c750b307a09d7051c03b9732f03e87b9505915d1d7d08edf2fffebe1b',
'what ethers 6 signMessage gives');
assert.ok(verifyEip191(ethAddress(m, 0), 'lineage/v0 keyring', sig));
assert.ok(!verifyEip191(ethAddress(m, 1), 'lineage/v0 keyring', sig));
assert.ok(!verifyEip191(ethAddress(m, 0).toLowerCase(), 'lineage/v0 keyring', sig), 'addresses are written in EIP-55 case');
assert.ok(!verifyEip191(ethAddress(m, 0), 'lineage/v0 keyring', sig.toUpperCase()));
});
test('nostr: the proof signs a kind-1 note at created_at 0, an event nostr-tools checks', () => {
const secret = bytesToHex(sha256(utf8('nostr'))), pubkey = bytesToHex(schnorr.getPublicKey(secret));
const npub = 'npub1kr98zklf2z4ywcfk97puw99dd3afyweelfz2leff8cjndn0qkquse7kw5l';
assert.equal(bech32.encode('npub', bech32.toWords(schnorr.getPublicKey(secret))), npub);
// The event's id, as NIP-01 serializes it; nostr-tools 2 computes the same id and accepts the signature.
assert.equal(bytesToHex(sha256(utf8(JSON.stringify([0, pubkey, 0, 1, [], 'lineage/v0 keyring'])))), '5d8dc7040e8128f597c0184f7bf347831e91c0051457c0811fae09ceb66200c5');
const sig = signNostr('lineage/v0 keyring', secret);
assert.equal(sig, '393b22965c0ea7cde5cfa67c4bc231a97b68d0441dfe4f99b123af45dc8add7a87ce9b52c50e46741d938277cf1fd55673442b9209054c329e325ba4530844d1');
assert.ok(verifyNostr(npub, 'lineage/v0 keyring', sig));
assert.ok(!verifyNostr(npub, 'lineage/v0 keyring\n', sig));
assert.ok(!verifyNostr(npub.toUpperCase(), 'lineage/v0 keyring', sig));
});
// Four keys from a label, for events that need no lab: Taproot and SegWit on regtest, an npub, an Ethereum address.
function ring(label) {
const secrets = Object.fromEntries(KEYS.map(k => [k, bytesToHex(sha256(utf8(`${label}/${k}`)))]));
const keys = {
taproot: bech32m.encode('bcrt', [1, ...bech32m.toWords(schnorr.getPublicKey(secrets.taproot))]),
segwit: bech32.encode('bcrt', [0, ...bech32.toWords(ripemd160(sha256(secp256k1.getPublicKey(secrets.segwit, true))))]),
nostr: bech32.encode('npub', bech32.toWords(schnorr.getPublicKey(secrets.nostr))),
ethereum: ethAddressOf(secp256k1.getPublicKey(secrets.ethereum)),
};
return { keys, secrets };
}
test('a keyring binds four keys, each signing the statement; any change breaks it', () => {
const node = `${'a'.repeat(64)}i0`, { keys, secrets } = ring('a'), m = signKeyring(node, keys, secrets), text = statement(node, keys);
assert.equal(text, `lineage/v0 keyring\nnode: ${node}\ntaproot: ${keys.taproot}\nsegwit: ${keys.segwit}\nnostr: ${keys.nostr}\nethereum: ${keys.ethereum}`);
assert.deepEqual(Object.keys(m), [...KEYS, 'proof']);
assert.equal(keyringWhy(node, m), '');
assert.equal(keyringWhy(`${'b'.repeat(64)}i0`, m), 'the taproot key did not sign this keyring', 'proofs are for one node');
const b = ring('b');
for (const k of KEYS) assert.match(keyringWhy(node, { ...m, [k]: b.keys[k] }), /did not sign this keyring/);
const forged = { taproot: signBip322(keys.taproot, text, b.secrets.taproot), segwit: signBip322(keys.segwit, text, b.secrets.segwit),
nostr: signNostr(text, b.secrets.nostr), ethereum: signEip191(text, b.secrets.ethereum) };
for (const k of KEYS) assert.equal(keyringWhy(node, { ...m, proof: { ...m.proof, [k]: forged[k] } }), `the ${k} key did not sign this keyring`);
assert.equal(keyringWhy(node, { ...m, proof: undefined }), 'the taproot key did not sign this keyring');
assert.equal(keyringWhy(node, { ...m, taproot: keys.segwit }), 'taproot must be a Taproot address');
assert.equal(keyringWhy(node, { ...m, segwit: keys.taproot }), 'segwit must be a P2WPKH address');
const testnet = address => bech32.encode('tb', bech32.decode(address).words);
assert.equal(keyringWhy(node, { ...m, segwit: testnet(keys.segwit) }), 'its Taproot and SegWit addresses must be on one network');
assert.equal(keyringWhy(node, { ...m, nostr: keys.nostr.toUpperCase() }), 'nostr must be an npub');
assert.notEqual(keys.ethereum, keys.ethereum.toLowerCase());
assert.equal(keyringWhy(node, { ...m, ethereum: keys.ethereum.toLowerCase() }), 'ethereum must be an EIP-55 address');
assert.throws(() => signKeyring(node, { ...keys, nostr: 'npub1' }, secrets), /^Error: Nostr must be an npub\.$/);
// A key binds one slot, whichever network its address is written for.
assert.deepEqual(slots({ ...keys, segwit: testnet(keys.segwit), taproot: bech32m.encode('tb', bech32m.decode(keys.taproot).words) }), slots(keys));
});
let n = 0;
const ev = (op, parent = null, meta = {}, height = 1) => ({ id: `${String(++n).padStart(64, '0')}i0`, status: height === null ? 'pending' : 'confirmed',
height, metadata: { op, ...meta }, parent, protocol: PROTOCOL });
const keyring = (node, label, height, parent = node.id) => ev('keyring', parent, signKeyring(node.id, ring(label).keys, ring(label).secrets), height);
test('the reducer: a node binds its own keys, the newest keyring counts, and a graduation or close retires it', () => {
const root = ev('found', null, { name: 'acme' }), ops = ev('birth', root.id, { name: 'ops' }, 2),
bot = ev('birth', ops.id, { name: 'bot', kind: 'agent' }, 2), lead = ev('birth', root.id, { name: 'lead', kind: 'agent' }, 2);
const events = [root, ops, bot, lead,
keyring(ops, 'ops', 3), keyring(bot, 'bot', 3), keyring(lead, 'lead', 3),
keyring(lead, 'bot', 4), // bot's keys, with good proofs, but bot has them
keyring(ops, 'other', 4, root.id), // signed for ops, published by acme
keyring(ops, 'ops2', 5), // replaces ops's keyring and frees its first keys
keyring(lead, 'ops', 6), // so another node with those secrets can bind them
ev('graduate', root.id, { of: ops.id }, 7), // ops and bot get new keys: their keyrings retire
keyring(bot, 'bot', 8), // and bot's old keys are free again
ev('close', lead.id, {}, 9), keyring(lead, 'lead', 10)];
const s = reduce(events), log = s.log.slice(4).map(e => [e.op, e.ok ? 'ok' : e.why]);
assert.deepEqual(log, [['keyring', 'ok'], ['keyring', 'ok'], ['keyring', 'ok'], ['keyring', 'its taproot key is already bound to bot'],
['keyring', 'the taproot key did not sign this keyring'], ['keyring', 'ok'], ['keyring', 'ok'], ['graduate', 'ok'], ['keyring', 'ok'],
['close', 'ok'], ['keyring', 'the node is closed']]);
const node = e => s.nodes.get(e.id);
assert.deepEqual(node(ops).keyring, { id: events[9].id, beat: 5, keys: ring('ops2').keys, retired: { cause: 'graduate', by: ops.id, beat: 7 } });
assert.deepEqual(node(bot).keyring, { id: events[12].id, beat: 8, keys: ring('bot').keys, retired: null });
assert.deepEqual(node(lead).keyring.retired, { cause: 'close', by: lead.id, beat: 9 });
assert.deepEqual(node(lead).keyring.keys, ring('ops').keys);
assert.deepEqual([...s.bound.values()], [bot.id, bot.id, bot.id, bot.id], 'only current keyrings hold keys');
assert.equal(summary(s.log[4], s), 'keyring · ops');
assert.equal(reduce(events, { upTo: 6 }).nodes.get(ops.id).keyring.retired, null);
});
test('in the lab a node publishes its own keyring, a beat away from any graduation that retires it', () => {
const lab = emptyLab();
const root = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', root, { name: 'ops' }), two = act(lab, 'birth', root, { name: 'two' });
send(lab, root, ops, SAT); send(lab, root, two, SAT);
const bot = act(lab, 'birth', ops, { name: 'bot', kind: 'agent' });
send(lab, ops, bot, 0.1 * SAT);
const id = act(lab, 'keyring', bot, {}), s = lineageState(lab), ring = ringOf(lab.keys, s, s.nodes.get(bot));
assert.deepEqual(s.nodes.get(bot).keyring.keys, ring.keys);
assert.equal(ring.keys.ethereum, ethAddress(childMnemonic(lab.keys.roots[root], 0), 1), "account 1 of ops's seed");
const record = lab.chain.transactions.get(id.slice(0, -2));
assert.equal(record.inscription.parent, bot, 'bot acts as itself');
assert.equal(record.inscription.text, statement(bot, ring.keys), 'the body is what every key signed');
setTempo(lab, 'manual');
act(lab, 'graduate', root, { of: ops });
assert.throws(() => act(lab, 'keyring', bot, {}), /^Error: ops's graduation is cued, and it changes bot's keys\. Publish the keyring once it lands\.$/);
assert.throws(() => act(lab, 'keyring', ops, {}), /ops's graduation is cued, and it changes its keys/);
act(lab, 'keyring', two, {});
assert.throws(() => act(lab, 'graduate', root, { of: two }), /^Error: two's keyring is cued\. Graduate two once it lands, so the graduation retires it\.$/);
setTempo(lab, 'each');
const after = lineageState(lab), beat = lab.chain.blocks.length;
assert.deepEqual(after.nodes.get(bot).keyring.retired, { cause: 'graduate', by: ops, beat });
act(lab, 'keyring', bot, {});
const fresh = lineageState(lab).nodes.get(bot).keyring;
assert.equal(fresh.retired, null);
assert.notEqual(fresh.keys.taproot, ring.keys.taproot, 'new keys under its new root');
act(lab, 'graduate', root, { of: two });
assert.equal(lineageState(lab).nodes.get(two).keyring.retired.cause, 'graduate');
});