Files
teahubxyzandClaude Opus 5.5 c8c18b073e Orgchain v1.14: soulbound belts ride their seats' sats
A belt is soulbound: the reducer already bound it to the seat it names,
and now the lab inscribes it on that seat's own sat, so by ordinal theory
it goes wherever the seat goes and never sells apart from it. One reveal
carries an award, three coins in: the seat's, which the pointer puts the
belt in and the seat's own key signs, accepting it; the issuer's, the
parent; and the commit, which pays the fee. Both coins come back unchanged,
so an award needs no postage and costs two fees (1,000 sats). ord marks it
a reinscription and vindicated, since a pointer and an envelope past the
first input would have cursed it before the jubilee. The engine takes that
three-input reveal and nothing looser: the parent spent from the
inscriber's key, both coins returned unchanged, the pointer on an inscribed
sat of the first.

A belt is never a lot, and an award can't land in a tree that is listed or
sold. Trees count and split by inscribed sat, so a seat's belts stay on it
through every sale. Inspect shows a node's belts and ord's charms, and the
award form says whose sat the belt will ride. Belts from before v1.14 stay
with their issuers and count the same.

Rehearsed on Bitcoin Core regtest from height 161: sixteen beats and 131
transactions; ord charms the three belts as the lab does, and after two
whole-tree sales each belt shares its seat's satpoint. The compare now
checks satpoints and charms.

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

245 lines
18 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, utf8ToBytes } from '@noble/hashes/utils';
import { sha256 } from '@noble/hashes/sha2';
import { schnorr } from '@noble/curves/secp256k1';
import { base64 } from '@scure/base';
import { Address, Tap } from '@cmdcode/tapscript';
import { PATHS, bip85Entropy, childMnemonic, ethAddress, ethSecret, nostrKey, nostrSecret, seedBytes, segwitPubkey, segwitSecret, 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, send } from '../src/orgchain/actions.js';
import { SAT } from '../src/orgchain/money.js';
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';
// 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/);
});
// 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';
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');
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));
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 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$/);
});
const at = (lab, id) => scriptHex(holderCoin(lab.chain, id).scriptPubKey);
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 Taproot address', () => {
const lab = emptyLab();
const root = act(lab, 'found', null, { name: 'acme' }), ops = act(lab, 'birth', root, { name: 'ops' });
send(lab, root, ops, SAT); // every act pays its own gas
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)), 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" },
tapkey: { address: Address.p2tr.fromPubKey(tapkeyOf(nostrKey(opsSeed, 1).pubkey), 'regtest'), path: "m/44'/1237'/1'/0/0" },
npub: { address: nostrKey(opsSeed, 1).npub, path: "m/44'/1237'/1'/0/0" },
ethereum: { address: ethAddress(opsSeed, 1), path: "m/44'/60'/1'/0/0" },
});
assert.match(a.taproot.address, /^bcrt1p/);
assert.match(a.segwit.address, /^bcrt1q/);
// 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);
const other = act(lab, 'found', null, { name: 'other' });
assert.notEqual(lab.keys.roots[other], m);
});
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' });
send(lab, root, ops, SAT);
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$/);
});
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' });
send(lab, root, ops, SAT);
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);
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))})`),
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');
});
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' });
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' });
const seat = act(lab, 'birth', studio, { name: 'seat', kind: 'agent' }), solo = act(lab, 'birth', studio, { name: 'solo' });
send(lab, studio, solo, 0.1 * SAT); // a graduate pays for the claim on its own root
const old = childMnemonic(lab.keys.roots[root], 0);
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 })];
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)]);
const leftovers = lab.chain.coins(rootWallet(lab.keys, old).script).flatMap(c => (c.inscriptions ?? []).map(i => i.id));
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);
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);
// 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);
});
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');
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');
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';
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.scheme, 'wallet');
assert.match(secretsOf(lab.keys, s, s.nodes.get(seat)).account[1][2], /^rawtr\(c/, 'an untweaked key imports as rawtr()');
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/);
});
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);
assert.equal(leavesOf(lab.keys, s, s.nodes.get(kid)), null);
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: 'bip86', roots: {}, adopted: {} });
assert.throws(() => keysPreference({ roots: { [`${'a'.repeat(64)}i0`]: 'not a mnemonic' } }), /Invalid lab mnemonic/);
});