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
This commit is contained in:
teahubxyzandClaude Opus 5.5 committed 2026-10-15 10:48:28 -07:00
1 parent 7c9306402d
commit 6998ae47aa
22 files changed
+673 -84

No files matched your search

+7 -2
View File
@@ -24,8 +24,13 @@ Orgchain is lineage's app for creating and operating lineage holdings on Super T
- 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 account's BIP-86 Taproot key (`m/86'/1'/account'/0/0`); SegWit is BIP-84 at the same
account and holds its money. 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.
account and holds its money; its Ethereum key is BIP-44 at `m/44'/60'/account'/0/0` (an org's, at account 0, is the
key tree page's legal-control key, and `tests/keyring.test.js` pins that page's vector). 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.
- Keyrings (`src/orgchain/keyring.js`): every key signs the statement with its own chain's message standard (BIP-322,
EIP-191, a NIP-01 event), so real wallets can make and check the proofs. Keep those formats exact and their pinned
vectors passing; a new chain's key joins only with its own standard and a vector to pin.
- Secrets show only behind Reveal, reset on every selection, and match the key tree page byte for byte (the tests pin
hashes of the page's own output).
- The page makes no network requests; keep it that way (the deployed CSP sets `connect-src 'none'`).
+6 -4
View File
@@ -14,14 +14,16 @@ The upstream CC0 legal text is retained unchanged at [`public/licenses/supertest
Runtime dependencies are pinned in `package.json` and `package-lock.json`:
- **@cmdcode/tapscript 1.5.3**, Christopher Scott, [upstream](https://github.com/cmdruid/tapscript), CC-BY-1.0. Used for transaction encoding, signing/verification, Taproot commitments and addresses, and the BIP-143 digests and signatures of SegWit payments. Installed package license retained unchanged at [`public/licenses/tapscript.txt`](public/licenses/tapscript.txt). Library source is unmodified; Vite bundles/minifies it for the browser.
- **@noble/curves 1.9.7**, Paul Miller and contributors, [upstream](https://github.com/paulmillr/noble-curves), MIT. Used for key generation/validation and public keys, and to verify the low-S DER signatures of SegWit payments: secp256k1 for chain and nostr keys, Ed25519 and X25519 for lineage's leaves. License retained at [`public/licenses/noble-curves.txt`](public/licenses/noble-curves.txt).
- **@noble/hashes 1.8.0**, Paul Miller and contributors, [upstream](https://github.com/paulmillr/noble-hashes), MIT. Used for synthetic block hashes, BIP-85's HMAC, the leaves' HKDF-SHA256 and the RIPEMD-160 of P2WPKH key hashes. License retained at [`public/licenses/noble-hashes.txt`](public/licenses/noble-hashes.txt).
- **@scure/base 1.2.6**, Paul Miller, [upstream](https://github.com/paulmillr/scure-base), MIT. Used for bech32 npubs and base64 leaf keys. License retained at [`public/licenses/scure-base.txt`](public/licenses/scure-base.txt).
- **@scure/bip32 1.7.0**, Patricio Palladino and Paul Miller, [upstream](https://github.com/paulmillr/scure-bip32), MIT. Used for HD derivation under the BIP-85, BIP-86, BIP-84 and NIP-06 paths. License retained at [`public/licenses/scure-bip32.txt`](public/licenses/scure-bip32.txt).
- **@noble/curves 1.9.7**, Paul Miller and contributors, [upstream](https://github.com/paulmillr/noble-curves), MIT. Used for key generation/validation and public keys, to verify the low-S DER signatures of SegWit payments, and to make and check keyrings' proofs (BIP-340, low-S ECDSA, and the public-key recovery EIP-191 relies on): secp256k1 for chain, nostr and Ethereum keys, Ed25519 and X25519 for lineage's leaves. License retained at [`public/licenses/noble-curves.txt`](public/licenses/noble-curves.txt).
- **@noble/hashes 1.8.0**, Paul Miller and contributors, [upstream](https://github.com/paulmillr/noble-hashes), MIT. Used for synthetic block hashes, BIP-85's HMAC, the leaves' HKDF-SHA256, the RIPEMD-160 of P2WPKH key hashes, the tagged hashes of BIP-322, and the Keccak-256 of Ethereum addresses (EIP-55) and EIP-191. License retained at [`public/licenses/noble-hashes.txt`](public/licenses/noble-hashes.txt).
- **@scure/base 1.2.6**, Paul Miller, [upstream](https://github.com/paulmillr/scure-base), MIT. Used for bech32 npubs, base64 leaf keys, and the bech32 and bech32m addresses and base64 BIP-322 signatures in keyrings. License retained at [`public/licenses/scure-base.txt`](public/licenses/scure-base.txt).
- **@scure/bip32 1.7.0**, Patricio Palladino and Paul Miller, [upstream](https://github.com/paulmillr/scure-bip32), MIT. Used for HD derivation under the BIP-85, BIP-86, BIP-84, NIP-06 and BIP-44 Ethereum paths. License retained at [`public/licenses/scure-bip32.txt`](public/licenses/scure-bip32.txt).
- **@scure/bip39 1.6.0**, Patricio Palladino and Paul Miller, [upstream](https://github.com/paulmillr/scure-bip39), MIT. Used for the 24-word lab roots and their seeds. License retained at [`public/licenses/scure-bip39.txt`](public/licenses/scure-bip39.txt).
Five local changes to the engine, all original code in `src/adapters/chain.js`, tested in `tests/money.test.js`, with Super Testnet's tests (`tests/adapters.test.js`) passing unchanged. Orgchain v1.5: the chain also accepts native SegWit (P2WPKH) outputs and SIGHASH_ALL P2WPKH spends, verified per BIP-143, so lineage accounts can hold and send test BTC at their BIP-84 addresses; everything about inscriptions stays Taproot-only. Orgchain v1.6: a coinbase may split the block reward across up to 64 outputs (`mineTo`; `mine` still pays one script), so one beat can pay several holdings. Orgchain v1.7: ordinary (non-reveal) spends may take up to eight inputs and outputs (`MAX_IO`), and their key-path Taproot signatures may carry SIGHASH_SINGLE|ANYONECANPAY (0x83, the sell-order flag), so one transaction can carry a deal: an item, the buyer's coins, the price and change. Reveals keep their two-in, two-out shape and default signatures. Orgchain v1.9: a chain may start from coins imported from a real chain (`importCoin`; `MetadataIndex` numbers their sats the same way, `importRanges` in `src/adapters/sats.js`), so the regtest rehearsal can broadcast the lab's transactions byte for byte; and a reveal's script path is checked per BIP-341 and BIP-342 against the exact script bytes (`tapscriptValid`), with tapscript's own check kept as a fallback so labs saved earlier still load. That rehearsal also found that the inscription adapter (`src/adapters/inscription.js`, adapted from inscriptions-online) committed its reveals to tapscript's `encodeScript` of a hex script, which re-encodes one-byte pushes, so real Bitcoin rejected them; it now commits to the exact bytes (`tapleafOf`). Tested in `tests/indexer.test.js`.
Keyrings (`src/orgchain/keyring.js`, original code) sign with each chain's own message standard: BIP-322 simple signatures, EIP-191 `personal_sign`, and a NIP-01 event. `tests/keyring.test.js` pins BIP-322's published vectors, and values that bip322-js 3.0.0, ethers 6 and nostr-tools 2 produced or accepted in a one-time cross-check; none of those is a dependency.
Vite and Playwright are build/test dependencies, not runtime network services. Their installed package notices remain in the locked dependency installation. No unlicensed upstream code was copied.
The bounded JSON/CBOR codec, byte-derived MetadataIndex, trusted event-counter reducer, local API facade and persisted auto-miner schedule are original adapter code. Metadata encoding and endpoint naming were checked against the [Ordinals metadata](https://docs.ordinals.com/inscriptions/metadata.html) and [endpoint](https://docs.ordinals.com/inscriptions/recursion.html) references; no ord source code was copied.
+33 -23
View File
@@ -6,12 +6,12 @@ Lineage gives an org its structure, a key tree with a place for every org and ag
inscription on a private toy chain in your browser, and its parent inscription is the node that authorizes it, so the
chain itself records who did what, and when.
- **Found** a holding (a root: the LLC). **Birth** orgs and agents under any org. **Award** a belt (one skill, one
seat, in any holding: a registry like well-known certifies agents it doesn't hold). **Decide** (a decision on a
node's record). **Make** an item (an inscription its maker holds, and can sell). **Graduate** an org: its edge flips
from derived to adopted, and it becomes a holding of its own, with its own tree, timeline and share of the mining,
while its parent's tree keeps a row for it. **Close** a node (derived children close with it; graduated ones hold
their own roots and stay open).
- **Found** a holding (a root: the LLC). **Birth** orgs and agents under any org. **Award** a belt (one skill, one seat,
in any holding: a registry like well-known certifies agents it doesn't hold). **Decide** (a decision on a node's
record). **Make** an item (an inscription its maker holds, and can sell). **Keyring** (a node publishes its keys on
each chain, each key signing the same statement). **Graduate** an org: its edge flips from derived to adopted, and it
becomes a holding of its own, with its own tree, timeline and share of the mining, while its parent's tree keeps a row
for it. **Close** a node (derived children close with it; graduated ones hold their own roots and stay open).
- **Time.** A beat is a block. At the default tempo each act is its own beat; at 10, 30 or 60 seconds, acts are cued and
land together on the next beat; on manual they wait for *Beat now*. The slider and *Replay* show a holding as it
stood at any beat.
@@ -39,24 +39,32 @@ chain itself records who did what, and when.
order book, until it is accepted, withdrawn, or goes stale (a coin it promised moved, or a side closed or re-keyed).
The buyer pays the fee; a price of 0 is a free claim. Payments spend coins no offer has promised first. Accepted
offers stay as the deals' receipts, and the timeline and Inspect show each deal (`src/orgchain/offers.js`).
- **Keyrings.** **Keyring** publishes a node's keys in one inscription whose parent is the node itself: its Taproot and
SegWit addresses, its npub and its Ethereum address, so another chain or a registry like well-known can find them
without deriving anything. Every key signs the same statement, the way its own wallets sign a message: BIP-322 for the
Bitcoin addresses, a nostr note for the npub, EIP-191 for Ethereum, so any wallet can make a proof and any library can
check one. The newest keyring counts, no key is bound to two nodes at once, and a graduation retires the keyrings it
re-keys, so the graduate publishes its new keys (the sample shows lineage's). Inspect shows each node's keyring and
the statement its keys signed (`src/orgchain/keyring.js`).
- **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; 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; its **Tapkey**, the npub as a Taproot
address (its nostr key with the BIP-86 tweak), which the npub alone finds; 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.
- **Secrets.** Each node's Inspect has a Reveal panel, as on lineage's key tree page: its seed (holdings and orgs),
its nsec, its Taproot, SegWit and Tapkey keys as descriptors Bitcoin Core imports as they are (test-network WIF,
BIP-380 checksums), and its leaves' private keys (OpenSSH, DKIM, WireGuard, PKCS#8, Headscale). Hidden again on
every selection; lab keys only.
inscriptions; **SegWit** (BIP-84, `m/84'/1'/account'/0/0`) for payments; its **Tapkey**, the npub as a Taproot address
(its nostr key with the BIP-86 tweak), which the npub alone finds; its **npub** (NIP-06); and its **Ethereum** address
(BIP-44, `m/44'/60'/account'/0/0`; an org's, at account 0, is its legal-control key, as on the key tree page). 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.
- **Secrets.** Each node's Inspect has a Reveal panel, as on lineage's key tree page: its seed (holdings and orgs), its
nsec, its Taproot, SegWit and Tapkey keys as descriptors Bitcoin Core imports as they are (test-network WIF, BIP-380
checksums), its Ethereum private key, and its leaves' private keys (OpenSSH, DKIM, WireGuard, PKCS#8, Headscale).
Hidden again on every selection; lab keys only.
Test coins and test keys only: a local chain, the lab wallet (the faucet that pays for foundings, and under *Mainnet*
the wallet holdings are funded from) and each holding's lab root, kept in this browser (`localStorage`, key
@@ -65,7 +73,8 @@ the operator's call.
## Events
`lineage/v0` is the ord metaprotocol (tag 7); the fields are CBOR metadata (tag 5); the body is a short text label.
`lineage/v0` is the ord metaprotocol (tag 7); the fields are CBOR metadata (tag 5); the body is a short text label (a
keyring's is the statement its keys signed).
| op | parent inscription (spent to authorize) | metadata |
| --- | --- | --- |
@@ -74,6 +83,7 @@ the operator's call.
| `award` | the issuing org or holding (`to` may sit in any holding) | `to`, `skill`, `belt` (`white`, `green`, `black`) |
| `decide` | the deciding node | `title`, `why` |
| `make` | the making node, which then holds the item | `name`, `note` |
| `keyring` | the node itself; the newest counts, and a graduation retires it | `taproot`, `segwit`, `nostr`, `ethereum`, and `proof` (each key's signature) |
| `graduate` | the child's parent (then the handoff to the child's fresh root); the child becomes a holding | `of` |
| `close` | the node itself | none |
@@ -92,10 +102,10 @@ fold in, why a parent proves authority, and every rule the reducer applies. The
node bin/lineage-index.mjs --ord http://127.0.0.1:8088 [--from <height>] [--compare <lab backup>]
```
`tools/regtest/` rehearses the sample on Bitcoin Core regtest: the lab's own transactions, byte for byte, confirmed by
Bitcoin Core and indexed by `ord`, which then tell the same story as the lab (`tests/indexer.test.js` replays a
recorded run). The first rehearsal found that the lab's reveals committed to the wrong tapleaf, which real Bitcoin
rejects; reveals are now checked the way Bitcoin checks them (`SPEC.md`, section 1).
`tools/regtest/` rehearses the sample, keyrings included, on Bitcoin Core regtest: the lab's own transactions, byte for
byte, confirmed by Bitcoin Core and indexed by `ord`, which then tell the same story as the lab (`tests/indexer.test.js`
replays a recorded run). The first rehearsal found that the lab's reveals committed to the wrong tapleaf, which real
Bitcoin rejects; reveals are now checked the way Bitcoin checks them (`SPEC.md`, section 1).
## Build and test
+58 -14
View File
@@ -15,9 +15,9 @@ lineage/v0 uses these fields of the ord envelope ([inscriptions](https://docs.or
| tag | field | use |
| --- | --- | --- |
| 7 | metaprotocol | exactly `lineage/v0`; inscriptions without it are not lineage's |
| 5 | metadata | a CBOR map: `op` and its fields (section 4), all strings, in 520-byte pushes (Orgchain keeps it under 2048 bytes) |
| 5 | metadata | a CBOR map: `op` and its fields (section 4), strings except a keyring's `proof` map, in 520-byte pushes (Orgchain keeps it under 2048 bytes) |
| 3 | parent | the inscription id of the node that authorizes the event; exactly one, except for `found` |
| 1 | content type | `text/plain;charset=utf-8`; the body is a short label such as `birth time`, ignored by the rules |
| 1 | content type | `text/plain;charset=utf-8`; the body is a short label such as `birth time` (a keyring's is its statement), ignored by the rules |
| 2 | pointer | puts a child's inscription in its own output when the parent is spent first |
The body, delegate (tag 11) and properties (tag 17) carry no meaning here; properties may later carry display traits
@@ -57,6 +57,7 @@ the inscription moves the authority.
| `award` | an active holding or org | `to`, `skill`, `belt` (`white`, `green` or `black`) | `to` (any other active node, in any holding) holds that belt in that skill, replacing its earlier belt in it |
| `decide` | an active node | `title` (required), `why` (optional) | a decision on the node's record |
| `make` | an active node | `name`, `note` (optional) | an item, made and first held by the node; a node makes one item per name |
| `keyring` | the node itself, active | `taproot`, `segwit`, `nostr`, `ethereum`, and `proof`: each key's signature | the node's keys are bound to it, replacing its earlier keyring (section 8) |
| `graduate` | the child's parent, active | `of` (an active org child, not yet adopted) | the edge becomes adopted, and the org becomes a holding of its own (section 5) |
| `close` | the node itself, active | none | the node closes, with every active derived descendant; adopted ones stay open |
@@ -69,7 +70,8 @@ breaks a rule is rejected: it stays on the chain and changes nothing. Any other
A holding is a root: a `found`, or an org that graduated. When an org graduates, it and its descendants still in the
old holding re-root under it, and it joins the holdings. Its parent stays its parent, so where it came from stays on the
record, and its old holding's tree keeps a row for it. A graduation event belongs to both holdings.
record, and its old holding's tree keeps a row for it. A graduation event belongs to both holdings. Its keys change with
its new root, and so do its derived descendants', so their keyrings retire (section 8).
In Orgchain a graduation is followed by a handoff: the org's fresh root claims its inscription, and the derived key that
held it countersigns. After that, only the new root can act for the org.
@@ -77,7 +79,9 @@ held it countersigns. After that, only the new root can act for the org.
## 6. Order-sensitive conflicts
Where independent events conflict within one block, the block's order decides. These are the cases: a close and an act
by one of its derived descendants; an award and its recipient's close; two foundings with the same name.
by one of its derived descendants; an award and its recipient's close; two foundings with the same name; a keyring and
a graduation or close that retires it; two keyrings that list the same key. Orgchain keeps a keyring and the graduation
that would retire it a block apart.
## 7. Ownership, items and deals
@@ -87,22 +91,62 @@ buyer's coins in, and the item to the buyer, the price to the holder and the buy
inputs first (SIGHASH_ALL) and the holder countersigns the item's. ord sees the item move, and nothing else needs
indexing.
## 8. Keys
## 8. Keys and keyrings
Indexers need no keys. Keys say where a node's inscription is expected to live, and who can sign for it. Each holding
has a 24-word root (BIP-39), and an org's seed is BIP-85 from its parent's (`m/83696968'/39'/0'/24'/i'`) until it
graduates. An org is account 0 of its seed and its agents are accounts 1 to n. Each account holds its inscription at its
BIP-86 Taproot address (`m/86'/coin'/account'/0/0`), keeps its money at BIP-84 SegWit, and has a NIP-06 npub.
Indexers need no keys to fold the trees. Keys say where a node's inscription is expected to live, and who can sign for
it. Each holding has a 24-word root (BIP-39), and an org's seed is BIP-85 from its parent's (`m/83696968'/39'/0'/24'/i'`)
until it graduates. An org is account 0 of its seed and its agents are accounts 1 to n. Each account holds its
inscription at its BIP-86 Taproot address (`m/86'/coin'/account'/0/0`), keeps its money at BIP-84 SegWit, and has a
NIP-06 npub (`m/44'/1237'/account'/0/0`) and an Ethereum address (`m/44'/60'/account'/0/0`). An org's Ethereum key, at
account 0, is its legal-control key: the one that holds its entity NFT.
A `keyring` publishes a node's keys, so other chains and registries can find them without deriving anything. Its parent
is the node itself. Its metadata lists four keys, each written the way its chain writes it, and a proof from each:
| field | key | proof, in `proof` under the same name |
| --- | --- | --- |
| `taproot` | the Taproot address that holds the node's inscription, lowercase | a BIP-322 simple signature, base64 |
| `segwit` | its P2WPKH address, lowercase, on the same network | a BIP-322 simple signature, base64 |
| `nostr` | its npub | the BIP-340 signature, in hex, of the NIP-01 event `{pubkey, created_at: 0, kind: 1, tags: [], content: statement}` |
| `ethereum` | its address, in EIP-55 case | an EIP-191 `personal_sign` signature: `0x`, then r, s and v in hex |
Every proof signs the same statement, one line each:
```
lineage/v0 keyring
node: <the node's inscription id>
taproot: <address>
segwit: <address>
nostr: <npub>
ethereum: <address>
```
The lines are joined by a line feed, with none after the last. Each key signs the way its own wallets sign a message, so
any wallet can make a proof and any library can check one. A Taproot proof is a 64-byte signature (SIGHASH_DEFAULT) or a
65-byte one ending in SIGHASH_ALL; a P2WPKH proof is a low-S DER signature with SIGHASH_ALL and the compressed key. The
inscription's body is the statement, for people to read; the rules rebuild it from the metadata.
A keyring is rejected unless every key is well formed and every proof checks out. While a node's keyring stands, its keys
are bound to that node, and a keyring that lists a key bound to another node is rejected; a key binds once, whatever
network it is written for. A node's newer keyring replaces its older one and frees the keys it no longer lists. A
graduation retires the keyrings of the graduate and its derived descendants, since their keys change with the new root,
and a close retires the keyrings of the nodes it closes. A retired keyring binds nothing, so the node publishes a new one.
Lineage's key tree page also makes an agent's keyring as a nostr event (kind 30078, `d` = `lineage/keyring/v0`), signed
by its npub and listing its leaves too. The inscription is the chain's record, with a proof from every key.
## 9. Versions and other chains
Tag 7 carries the version. A new version is a new string with its own rules, which v0 indexers ignore. The events and
the rules are chain-agnostic. What another chain must supply is its own proof of order and of authority (a signature
by the node's key where nothing is spent). Each holding's events would live on one home chain, and an org-signed
keyring would bind a node's keys across chains.
by the node's key where nothing is spent). Each holding's events would live on one home chain, and its nodes' keyrings
(section 8) bind their keys on the others. Another chain's key joins the keyring in a new version, proven the way that
chain's own wallets sign a message.
## 10. Conformance
`tools/regtest` rehearses Orgchain's sample on Bitcoin Core regtest, byte for byte, and `bin/lineage-index.mjs
--compare` checks that ord and the lab tell the same story: the same events in the same beats, the same metadata and
parents, the same trees, and every inscription in the same output. `tests/indexer.test.js` replays a recorded run.
`tools/regtest` rehearses Orgchain's sample on Bitcoin Core regtest, byte for byte, keyrings included, and
`bin/lineage-index.mjs --compare` checks that ord and the lab tell the same story: the same events in the same beats,
the same metadata and parents, the same trees and keyrings, and every inscription in the same output.
`tests/indexer.test.js` replays a recorded run. `tests/keyring.test.js` checks the proofs against BIP-322's published
vectors, the key tree page's Ethereum vector, and values cross-checked with bip322-js, ethers and nostr-tools.
+2
View File
@@ -21,6 +21,8 @@ for (const root of state.roots) {
const rejected = state.log.filter(e => !e.ok);
console.log(`\n${plural(events.length, 'lineage/v0 event')} on ord, ${plural(state.items.size, 'item')}${rejected.length ? `, ${rejected.length} rejected:` : ''}`);
for (const e of rejected) console.log(` ${summary(e, state)} (${e.id})`);
const rings = [...state.nodes.values()].filter(n => n.keyring && !n.keyring.retired);
if (rings.length) console.log(`${plural(rings.length, 'keyring')} bind ${plural(state.bound.size, 'key')}: ${rings.map(n => n.name).join(', ')}`);
if (values.compare) {
const { problems, ...n } = compareWithLab(decodeLab(readFileSync(values.compare, 'utf8')), events);
console.log(problems.length ? `\nord and the lab differ:\n${problems.map(p => ` ${p}`).join('\n')}`
+9 -3
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Orgchain · Lineage</title>
<meta name="description" content="Found and operate lineage holdings on a toy chain in your browser: birth orgs and agents, award belts, decide, make and trade items, send test BTC, graduate and close, one beat at a time. Test coins only.">
<meta name="description" content="Found and operate lineage holdings on a toy chain in your browser: birth orgs and agents, award belts, decide, make and trade items, publish keyrings, send test BTC, graduate and close, one beat at a time. Test coins only.">
<meta name="color-scheme" content="light dark">
<link rel="icon" href="./favicon.svg" type="image/svg+xml">
<script src="./theme.js"></script>
@@ -16,7 +16,7 @@
<div>
<p class="kicker"><a href="../">Lineage</a> /</p>
<h1 class="wordmark">Orgchain</h1>
<p class="lede">Found a <b>holding</b>, then birth its orgs and agents, award belts, record decisions, make and trade items, send test BTC, graduate and close. Each act is an inscription whose parent is the node that authorizes it, paid for with the actor's own gas, and each <b>beat</b> is a block whose 50 test BTC the holdings mine. Lineage gives the structure; the chain adds time.</p>
<p class="lede">Found a <b>holding</b>, then birth its orgs and agents, award belts, record decisions, make and trade items, publish keyrings, send test BTC, graduate and close. Each act is an inscription whose parent is the node that authorizes it, paid for with the actor's own gas, and each <b>beat</b> is a block whose 50 test BTC the holdings mine. Lineage gives the structure; the chain adds time.</p>
</div>
<div class="flags">
<span class="flag hz">Test coins only</span>
@@ -88,7 +88,7 @@
<p class="pad note" id="act-none">Select a holding, or a node in its tree, to act through it.</p>
<div class="pad note" id="act-past" hidden><p>You are looking at beat <b id="past-beat"></b>. History is read-only.</p><button type="button" class="btn sm" id="past-now">Back to now</button></div>
<div id="act-on" hidden>
<div class="verbs" role="group" aria-label="Verb"><button type="button" data-verb="birth" aria-pressed="true">Birth</button><button type="button" data-verb="award" aria-pressed="false">Award</button><button type="button" data-verb="decide" aria-pressed="false">Decide</button><button type="button" data-verb="make" aria-pressed="false">Make</button><button type="button" data-verb="send" aria-pressed="false">Send</button><button type="button" data-verb="offer" aria-pressed="false">Offer</button><button type="button" data-verb="graduate" aria-pressed="false">Graduate</button><button type="button" data-verb="close" aria-pressed="false">Close</button></div>
<div class="verbs" role="group" aria-label="Verb"><button type="button" data-verb="birth" aria-pressed="true">Birth</button><button type="button" data-verb="award" aria-pressed="false">Award</button><button type="button" data-verb="decide" aria-pressed="false">Decide</button><button type="button" data-verb="make" aria-pressed="false">Make</button><button type="button" data-verb="send" aria-pressed="false">Send</button><button type="button" data-verb="offer" aria-pressed="false">Offer</button><button type="button" data-verb="keyring" aria-pressed="false">Keyring</button><button type="button" data-verb="graduate" aria-pressed="false">Graduate</button><button type="button" data-verb="close" aria-pressed="false">Close</button></div>
<p class="keyline" id="act-key"></p>
<p class="keyline" id="act-gas"></p>
<p class="why pad" id="act-why"></p>
@@ -148,6 +148,12 @@
<p class="hint">Its SegWit key signs the whole deal now: the item to its Taproot key, the price to the holder, the 500-sat fee from its balance. Nothing reaches the chain until the holder accepts. Until then the offer stands, unless it's withdrawn or a coin it promised moves.</p>
<button class="btn primary go" type="submit">Offer</button>
</form>
<form class="vf pad" id="f-keyring" data-form="keyring" hidden>
<p class="vh"><b data-name></b> publishes its keyring: one inscription that lists its keys on each chain, each key signing the same statement. Publishing spends <b data-name></b>'s inscription.</p>
<dl class="fl ring" id="keyring-keys"></dl>
<p class="hint">Each key signs the way its own wallets sign a message: BIP-322 for the Bitcoin addresses, a nostr event for the npub, EIP-191 for Ethereum. No other node can list these keys while this keyring stands. A newer keyring replaces it, and a graduation retires it.</p>
<button class="btn primary go" type="submit">Publish keyring</button>
</form>
<form class="vf pad" id="f-graduate" data-form="graduate" hidden>
<p class="vh"><b data-parent></b> vouches for <b data-name></b>, and the edge flips from derived to adopted. A graduated org holds its own root and becomes a holding of its own, so it mines, and it stays open if <b data-parent></b> closes. The event spends <b data-parent></b>'s inscription.</p>
<button class="btn primary go" type="submit" id="graduate-go">Graduate</button>
+20 -1
View File
@@ -7,7 +7,8 @@ import { bech32 } from '@scure/base';
import { hmac } from '@noble/hashes/hmac';
import { sha512 } from '@noble/hashes/sha2';
import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils';
import { schnorr } from '@noble/curves/secp256k1';
import { keccak_256 } from '@noble/hashes/sha3';
import { schnorr, secp256k1 } from '@noble/curves/secp256k1';
export const PATHS = {
// BIP-85, application 39 (BIP-39), English, 24 words: lineage spec v0.1's org seed.
@@ -18,6 +19,8 @@ export const PATHS = {
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`,
// BIP-44 Ethereum. Account 0 is the org's legal-control key, the one the key tree page shows; an agent's is its account.
ethereum: account => `m/44'/60'/${account}'/0/0`,
};
const MAX_INDEX = 2 ** 31 - 1;
const masters = new Map(), children = new Map();
@@ -66,6 +69,22 @@ export function segwitPubkey(mnemonic, account = 0, coin = 1) {
export function nostrSecret(mnemonic, account = 0) {
return bytesToHex(master(mnemonic).derive(PATHS.nostr(assertIndex(account, 'account'))).privateKey);
}
// EIP-55: the hex address with each letter uppercased where the Keccak-256 of the lowercase address has a high nibble.
export function checksumAddress(hex40) {
const a = hex40.toLowerCase(), hash = bytesToHex(keccak_256(utf8ToBytes(a)));
return `0x${[...a].map((c, i) => (parseInt(hash[i], 16) >= 8 ? c.toUpperCase() : c)).join('')}`;
}
// An Ethereum address: the last 20 bytes of the Keccak-256 of the uncompressed public key, without its 04 prefix.
export const ethAddressOf = publicKey => checksumAddress(bytesToHex(keccak_256(secp256k1.ProjectivePoint.fromHex(publicKey).toRawBytes(false).slice(1)).slice(-20)));
export function ethSecret(mnemonic, account = 0) {
return bytesToHex(master(mnemonic).derive(PATHS.ethereum(assertIndex(account, 'account'))).privateKey);
}
const eth = new Map();
export function ethAddress(mnemonic, account = 0) {
const id = `${mnemonic}|${account}`;
if (!eth.has(id)) eth.set(id, ethAddressOf(master(mnemonic).derive(PATHS.ethereum(assertIndex(account, 'account'))).publicKey));
return eth.get(id);
}
export function nostrKey(mnemonic, account = 0) {
const pubkey = schnorr.getPublicKey(master(mnemonic).derive(PATHS.nostr(assertIndex(account, 'account'))).privateKey);
return { pubkey: bytesToHex(pubkey), npub: bech32.encode('npub', bech32.toWords(pubkey)) };
+42 -3
View File
@@ -4,7 +4,8 @@ import './orgchain.css';
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, setRigs, setRule, setTempo, tick } from './orgchain/store.js';
import { SAMPLE, accept, act, deposit, gasCheck, gasCost, lastHolding, minerOf, offer, send, seedSample, withdraw } from './orgchain/actions.js';
import { SAMPLE, accept, act, beatClash, deposit, gasCheck, gasCost, lastHolding, minerOf, offer, send, seedSample, withdraw } from './orgchain/actions.js';
import { KEYS, statement } from './orgchain/keyring.js';
import { addressesOf, derivedSubtree, holderOf, keyOf, leavesOf, secretsOf } from './orgchain/keys.js';
import { DUST, FEE, balances, brief, btc, coins, ledger, miners, most, owners, payerOf, rigsOf, satsOf, tankOf, tanks } from './orgchain/money.js';
import { holderOfItem, offerStatus, offerWhy } from './orgchain/offers.js';
@@ -242,6 +243,20 @@ function offerBlock(n) {
if (!biddable(n).length) return 'Nobody else holds an item yet. Items come from Make';
return lab.chain.coins(wallet.script).some(c => c.value >= FEE + DUST || c.value === FEE) ? '' : broke(n, 'to pay with');
}
// The keys n's keyring would list now, as addresses (orgchain/keys.js), or null for a holding without lab keys.
function ringKeys(n, state = live) {
const a = addressesOf(lab.keys, state, n);
return a && { taproot: a.taproot.address, segwit: a.segwit.address, nostr: a.npub.address, ethereum: a.ethereum.address };
}
// Why n cannot publish a keyring now ('' when it can): it needs lab keys, and a new one only when its keys changed.
function keyringBlock(n) {
const keys = ringKeys(n), ring = n.keyring;
if (!keys) return 'This holding predates lab keys, so it has no keys to list';
const clash = beatClash(live, 'keyring', n.id);
if (clash) return clash;
if (!ring || ring.retired || KEYS.some(k => ring.keys[k] !== keys[k])) return '';
return ring.beat === 'cued' ? `${n.name}'s keyring is cued for the next beat` : `${n.name}'s keyring already lists these keys, since beat ${ring.beat}`;
}
// Why a verb is unavailable for this node ('' when it is available). The reducer has the final word.
function blocked(n) {
const parent = node(n.parent), closed = n.status === 'closed' ? `${n.name} is closed` : '';
@@ -251,10 +266,12 @@ function blocked(n) {
award: closed || (n.kind === 'agent' ? 'Agents do not award belts' : recipients(n).length ? gas('award') : 'There is nobody else to award yet'),
decide: closed || gas('decide'),
make: closed || gas('make'),
keyring: closed || keyringBlock(n) || gas('keyring'),
send: closed || sendBlock(n),
offer: closed || offerBlock(n),
graduate: closed || (!parent ? 'A holding has no parent to graduate from' : n.kind !== 'org' ? 'Only orgs graduate'
: n.edge === 'adopted' ? `${n.name} has already graduated` : parent.status !== 'active' ? `${parent.name} is closed` : gas('graduate', parent.id, { of: n.id })),
: n.edge === 'adopted' ? `${n.name} has already graduated` : parent.status !== 'active' ? `${parent.name} is closed`
: beatClash(live, 'graduate', parent.id, { of: n.id }) || gas('graduate', parent.id, { of: n.id })),
close: closed || gas('close'),
};
}
@@ -285,6 +302,8 @@ function renderAct() {
}
for (const id of ['send-has', 'offer-has']) $(id).textContent = btc(held(n.id));
$('graduate-go').textContent = `Graduate ${n.name}`;
const keys = ui.verb === 'keyring' && !why.keyring && ringKeys(n);
if (keys) $('keyring-keys').replaceChildren(...[['Taproot', keys.taproot], ['SegWit', keys.segwit], ['nostr', keys.nostr], ['Ethereum', keys.ethereum]].map(fact));
const signer = ui.verb === 'graduate' ? parent : n, k = signer && keyOf(lab.keys, live, signer);
const pays = ui.verb === 'send' || ui.verb === 'offer', path = k && (pays ? `${k.from} · ${payerOf(lab.keys, live, signer).path}` : k.path);
$('act-key').textContent = !signer || why[ui.verb] ? '' : k ? `Signs as ${signer.name}: ${path}` : 'Signs with the lab wallet: this holding predates lab keys.';
@@ -429,16 +448,31 @@ function keysView(n) {
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}. Its money: ${n.kind === 'holding' ? 'the beats it earns land here, and ' : ''}sends come and go here.`],
['Tapkey', a.tapkey.address, 'Its npub as a Taproot address (its nostr key with the BIP-86 tweak), so the npub alone finds it. Nostr secrets get pasted into clients, so treat it as an inbox.'],
['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];
['npub', a.npub.address, `NIP-06 at ${a.npub.path}. Its nostr name; the leaves below hang off it.`],
['Ethereum', a.ethereum.address, agent ? `BIP-44 at ${a.ethereum.path}: account ${k.account} of its org's seed, the way Ledger Live numbers Ethereum accounts.`
: `BIP-44 at ${a.ethereum.path}. Its legal-control key, the one that holds the entity NFT, as on the lineage key tree page.`],
['From', `${k.from} · account ${k.account}`], held, ringFact(n)];
return h('div', { class: 'keys' },
h('p', { class: 'lab', text: 'Keys' }),
h('dl', { class: 'fl' }, facts.map(fact)),
n.keyring && copyBlock(`${n.keyring.retired ? 'Retired keyring' : 'Keyring'} · what each key signed`, statement(n.id, n.keyring.keys)),
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 })))),
copyBlock(agent ? 'Public lines to paste' : 'CA key for TrustedUserCAKeys', lines.join('\n\n')),
secretsView(n, k));
}
// A node's keyring as of the viewed beat (orgchain/keyring.js): the inscription that binds its keys, each key signing it.
function ringFact(n) {
const ring = n.keyring, by = ring?.retired && (ring.retired.by === n.id ? 'It' : node(ring.retired.by, view)?.name ?? 'Its parent');
if (!ring) return ['Keyring', 'none yet', 'Keyring publishes one: an inscription that lists these keys, each key signing it.'];
if (!ring.retired) {
return ['Keyring', `published at ${at(ring.beat)}`, 'Each key signed it: Taproot and SegWit by BIP-322, the npub as a nostr event, Ethereum by EIP-191.'];
}
return ['Keyring', `retired at ${at(ring.retired.beat)}`, ring.retired.cause === 'close' ? `${by} closed, which freed the keys it listed.`
: by === 'It' ? 'It graduated, so its keys changed with its new root. Keyring publishes a new one.'
: `${by} graduated, so ${n.name}'s keys changed with ${by}'s new root. Keyring publishes a new one.`];
}
// A code block with a Copy button; shown can lay the text out differently from what is copied.
function copyBlock(label, text, shown = text) {
const pre = h('pre', { text: shown });
@@ -599,6 +633,11 @@ $('f-make').addEventListener('submit', async event => {
if (!name) return say('Name the item: lowercase letters, digits and hyphens.', true);
if (await perform('make', n.id, { name, note }, (id, where) => `${n.name} made ${name}, ${where}.`)) form.reset();
});
$('f-keyring').addEventListener('submit', async event => {
event.preventDefault();
const n = node(ui.node);
await perform('keyring', n.id, {}, (id, where) => `${n.name} published its keyring, ${where}: four keys, each signing the same statement.`);
});
$('f-offer').addEventListener('submit', async event => {
event.preventDefault();
const form = event.currentTarget, n = node(ui.node), item = $('offer-item').value, price = satsOf(form.elements.price.value);
+4
View File
@@ -326,3 +326,7 @@ textarea { resize: vertical; line-height: 1.5; }
.bids { list-style: none; margin: 6px 0 0; padding: 0 0 0 12px; border-left: 2px solid var(--ink-3); display: grid; gap: 6px; }
.ev.money.deal .op { color: var(--ink); }
.f dd .btn.open { margin-left: 8px; vertical-align: middle; }
.ring { margin: 2px 0 4px; }
.ring .f { padding-top: 8px; margin-top: 8px; }
.ring .f:first-child { margin-top: 0; padding-top: 0; }
.ring .f dd { font: var(--t-xs)/1.5 var(--mono); }
+40 -10
View File
@@ -1,4 +1,4 @@
// Orgchain's verbs as lineage/v0 inscriptions: found, birth, award, decide, make, graduate, close. Beside them, money
// Orgchain's verbs as lineage/v0 inscriptions: found, birth, award, decide, make, keyring, graduate, close. Beside them, money
// moves: sends, offers on items and their acceptance, and funding a holding from the lab wallet.
// Each act is signed by the key that holds its authorizing node's inscription (lineage's key tree, ./keys.js).
// Gas: every act is paid by the acting node's own keys, topped up from its own money (./money.js); the lab wallet
@@ -12,21 +12,46 @@ import { newMnemonic } from '../adapters/lineage-keys.js';
import { holderCoin, makeClaim, countersign } from '../adapters/handoff.js';
import { PROTOCOL, lineageEvents, reduce } from './lineage.js';
import { armClock, beat } from './store.js';
import { derivedSubtree, emptyKeys, holderOf, keyOf, rootWallet, scriptHex } from './keys.js';
import { derivedSubtree, emptyKeys, holderOf, keyOf, ringOf, rootWallet, scriptHex } from './keys.js';
import { KEYS, signKeyring, statement } from './keyring.js';
import { DUST, REWARD, SAT, canSpend, covers, pay, payouts, payerOf, spend, sweep } from './money.js';
import { acceptOffer, makeOffer, withdrawOffer } from './offers.js';
const FIELDS = { found: ['name'], birth: ['name', 'kind'], award: ['to', 'skill', 'belt'], decide: ['title', 'why'], make: ['name', 'note'],
graduate: ['of'], close: [] };
keyring: KEYS, graduate: ['of'], close: [] };
// A keyring's body is its statement, so an explorer shows what every key signed. The rules rebuild it from the metadata.
const BODY = { found: m => `found ${m.name}`, birth: m => `birth ${m.name}`, award: m => `award ${m.skill} ${m.belt}`,
decide: m => `decide ${m.title}`, make: m => `make ${m.name}`, graduate: () => 'graduate', close: () => 'close' };
decide: m => `decide ${m.title}`, make: m => `make ${m.name}`, keyring: (m, by) => statement(by, m), graduate: () => 'graduate', close: () => 'close' };
export function metadataFor(op, fields = {}) {
if (!Object.hasOwn(FIELDS, op)) throw new Error('Unknown verb.');
const metadata = { op };
for (const key of FIELDS[op]) { const v = fields[key]; if (typeof v === 'string' && v.trim()) metadata[key] = v.trim(); }
const proof = op === 'keyring' && fields.proof && typeof fields.proof === 'object' ? fields.proof : null;
if (proof) metadata.proof = Object.fromEntries(KEYS.filter(k => typeof proof[k] === 'string').map(k => [k, proof[k]]));
return metadata;
}
// A keyring is the one act whose fields the lab fills in: the node's keys, each signing the statement (./keyring.js).
function keyringFields(lab, by) {
const state = lineageState(lab), n = state.nodes.get(by), ring = n && ringOf(lab.keys, state, n);
if (!ring) throw new Error(`${n?.name ?? 'That node'} predates lab keys, so it has no keys to list.`);
return signKeyring(by, ring.keys, ring.secrets);
}
// A graduation re-keys its org's derived subtree and retires the keyrings there. Within one block the chain does not
// order it against a keyring in that subtree (the two spend different inscriptions), so the lab keeps them a beat apart.
export function beatClash(state, op, by, fields = {}) {
if (op === 'keyring') {
for (let n = state.nodes.get(by); n; n = n.edge === 'derived' ? state.nodes.get(n.parent) : null) {
if (n.graduated === 'cued') return `${n.name}'s graduation is cued, and it changes ${n.id === by ? 'its' : `${state.nodes.get(by).name}'s`} keys. Publish the keyring once it lands`;
}
}
if (op === 'graduate' && state.nodes.has(fields.of)) {
const subtree = new Set(derivedSubtree(state, fields.of).map(n => n.id));
const cued = state.log.find(e => e.pending && e.ok && e.op === 'keyring' && subtree.has(e.node));
if (cued) return `${state.nodes.get(cued.node).name}'s keyring is cued. Graduate ${state.nodes.get(fields.of).name} once it lands, so the graduation retires it`;
}
return '';
}
export const lineageState = lab => reduce(lineageEvents(new MetadataIndex(lab.chain)), { pending: true });
@@ -100,7 +125,7 @@ function hand(lab, id, from, to, payer = from) {
lab.chain.submitPackage([signed(tx, [from, payer])]);
}
function inscribe(lab, wallet, op, by, metadata) {
return new InscriptionAdapter(lab.chain, wallet).inscribe(BODY[op](metadata), by ?? null, metadata, { metaprotocol: PROTOCOL });
return new InscriptionAdapter(lab.chain, wallet).inscribe(BODY[op](metadata, by), by ?? null, metadata, { metaprotocol: PROTOCOL });
}
// What a beat pays (money.js payouts): every mining holding's share, or, under 'last', the holding that acted.
const minerFor = (lab, id) => { const state = lineageState(lab); return payouts(lab, state, state.nodes.get(id)?.root ?? null); };
@@ -169,7 +194,9 @@ function perform(lab, op, by, metadata) {
// One act is one inscription whose parent is the authorizing node. At the 'each' tempo it lands at once; otherwise it is cued.
export function act(lab, op, by, fields, now = Date.now()) {
const metadata = check(lab, op, by, fields), why = gasCheck(lab, lineageState(lab), op, by ?? null, metadata);
const clash = beatClash(lineageState(lab), op, by ?? null, fields ?? {});
if (clash) throw new Error(`${clash}.`);
const metadata = check(lab, op, by, op === 'keyring' ? keyringFields(lab, by) : fields), why = gasCheck(lab, lineageState(lab), op, by ?? null, metadata);
if (why) throw new Error(`${why}.`);
fund(lab);
const id = perform(lab, op, by ?? null, metadata);
@@ -211,11 +238,12 @@ export function deposit(lab, holdingId, amount, now = Date.now()) {
// The drawing in docs/lineage.md, rehearsed over six beats. Names stand in for inscription ids until those exist.
// Its beats pay yolopreneur, and sends pass money down the tree before each org acts, since every act pays its gas.
// It ends on a standing offer: lineage's bid for the launch kit sol made, there for the holder to accept.
// Three nodes publish keyrings, and lineage's retires when it graduates (its keys change with its root). It ends on a
// standing offer: lineage's bid for the launch kit sol made, there for the holder to accept.
export const SAMPLE = 'yolopreneur';
const PLAN = [
[['found', null, { name: 'yolopreneur' }]],
[['birth', 'yolopreneur', { name: 'well-known', kind: 'org' }], ['birth', 'yolopreneur', { name: 'time', kind: 'org' }]],
[['birth', 'yolopreneur', { name: 'well-known', kind: 'org' }], ['birth', 'yolopreneur', { name: 'time', kind: 'org' }], ['keyring', 'yolopreneur', {}]],
[['send', 'yolopreneur', { to: 'time', amount: 40 }], ['send', 'yolopreneur', { to: 'well-known', amount: 10 }],
['birth', 'well-known', { name: 'mailr', kind: 'org' }], ['birth', 'well-known', { name: 'lineage', kind: 'org' }],
['birth', 'time', { name: 'luna', kind: 'agent' }], ['birth', 'time', { name: 'sol', kind: 'agent' }], ['birth', 'time', { name: 'astra', kind: 'agent' }]],
@@ -225,7 +253,8 @@ const PLAN = [
[['send', 'time', { to: 'sol', amount: 1 }], ['send', 'time', { to: 'luna', amount: 0.5 }],
['award', 'time', { to: 'sol', skill: 'launch', belt: 'green' }], ['award', 'time', { to: 'luna', skill: 'post', belt: 'white' }],
['decide', 'yolopreneur', { title: 'Every venture gets a page', why: '<venture>.yolopreneur.lol, served from one pages branch.' }],
['make', 'sol', { name: 'launch-kit', note: 'The page, the post and the checklist every venture launches with.' }]],
['make', 'sol', { name: 'launch-kit', note: 'The page, the post and the checklist every venture launches with.' }],
['keyring', 'sol', {}], ['keyring', 'lineage', {}]],
[['graduate', 'well-known', { of: 'lineage' }],
['decide', 'lineage', { title: 'Lineage gets its own org', why: 'Its own repo on Teahub now, lineage.llc later: adopted, not derived.' }],
['offer', 'lineage', { item: 'launch-kit', price: 0.1 }]],
@@ -238,7 +267,8 @@ export function seedSample(lab) {
for (const [op, by, fields] of group) {
if (op === 'send') { pay(lab, lineageState(lab), ids[by], ids[fields.to], fields.amount * SAT); continue; }
if (op === 'offer') { makeOffer(lab, items[fields.item], ids[by], fields.price * SAT); continue; }
const resolved = Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, k === 'to' || k === 'of' ? ids[v] : v]));
const resolved = op === 'keyring' ? keyringFields(lab, ids[by])
: Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, k === 'to' || k === 'of' ? ids[v] : v]));
const parent = by ? ids[by] : null, id = perform(lab, op, parent, check(lab, op, parent, resolved));
if (op === 'found' || op === 'birth') ids[fields.name] = id;
if (op === 'make') items[fields.name] = id;
+176
View File
@@ -0,0 +1,176 @@
// Keyrings (SPEC.md section 8): one inscription that binds a node's keys across chains. Every key signs the same
// statement, the way its own wallets sign a message: BIP-322 for the Bitcoin addresses, a NIP-01 event for the npub and
// EIP-191 for the Ethereum address. So any wallet can make or check a proof. Listing a key proves nothing; signing does.
import { schnorr, secp256k1 } from '@noble/curves/secp256k1';
import { sha256 } from '@noble/hashes/sha2';
import { keccak_256 } from '@noble/hashes/sha3';
import { ripemd160 } from '@noble/hashes/legacy';
import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from '@noble/hashes/utils';
import { base64, bech32, bech32m } from '@scure/base';
import { checksumAddress, ethAddressOf } from '../adapters/lineage-keys.js';
// The keys a keyring lists, in the statement's order: where the node holds its inscription (Taproot), where it keeps
// its money (SegWit), its nostr name, and its Ethereum address (an org's is its legal-control key).
export const KEYS = ['taproot', 'segwit', 'nostr', 'ethereum'];
// What every key signs: the protocol, the node (its inscription id) and each key, one per line.
export const statement = (node, keys) => ['lineage/v0 keyring', `node: ${node}`, ...KEYS.map(k => `${k}: ${keys[k]}`)].join('\n');
const NETWORKS = ['bc', 'tb', 'bcrt'];
const AUX = new Uint8Array(32); // BIP-340 lets aux be zeros: the same keyring then signs to the same bytes
const u32 = n => { const b = new Uint8Array(4); new DataView(b.buffer).setUint32(0, n, true); return b; };
const sized = b => concatBytes(b.length < 0xfd ? Uint8Array.of(b.length) : Uint8Array.of(0xfd, b.length & 255, b.length >> 8), b);
const dsha = b => sha256(sha256(b));
const tagged = (tag, ...parts) => { const t = sha256(utf8ToBytes(tag)); return sha256(concatBytes(t, t, ...parts)); };
const same = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
// A lowercase SegWit address (BIP-173, BIP-350) on a Bitcoin network: its network, witness version and program.
export function decodeAddress(address) {
if (typeof address !== 'string' || address !== address.toLowerCase()) return null;
for (const [codec, version] of [[bech32, 0], [bech32m, 1]]) {
try {
const { prefix, words } = codec.decode(address, 90);
if (words[0] === version && NETWORKS.includes(prefix)) return { network: prefix, version, program: Uint8Array.from(codec.fromWords(words.slice(1))) };
} catch { /* the other encoding, or no address at all */ }
}
return null;
}
const isTaproot = a => a?.version === 1 && a.program.length === 32;
const isP2wpkh = a => a?.version === 0 && a.program.length === 20;
const scriptOf = ({ version, program }) => concatBytes(Uint8Array.of(version ? 0x50 + version : 0, program.length), program);
// BIP-322 "simple" signatures for single-key P2WPKH and Taproot (key path) addresses. A virtual to_spend transaction
// commits to the message and pays the address; to_sign spends it, and to_sign's witness, serialized, is the signature.
export const messageHash = message => tagged('BIP0322-signed-message', utf8ToBytes(message));
const ZERO = new Uint8Array(8); // the virtual transactions move 0 sats
const OUTPUTS = concatBytes(ZERO, sized(Uint8Array.of(0x6a))); // to_sign's one output: 0 sats to OP_RETURN
export function toSpendTxid(address, message) {
const script = scriptOf(address), scriptSig = concatBytes(Uint8Array.of(0, 32), messageHash(message));
return dsha(concatBytes(u32(0), Uint8Array.of(1), new Uint8Array(32), u32(0xffffffff), sized(scriptSig), u32(0),
Uint8Array.of(1), ZERO, sized(script), u32(0)));
}
// to_sign's sighash: BIP-143 (SIGHASH_ALL) for P2WPKH, BIP-341 for Taproot (SIGHASH_DEFAULT, or ALL when the signature
// carries the 0x01 byte). Both spend to_spend:0, with version, sequence and locktime 0.
function sighash(address, message, type = 0) {
const script = scriptOf(address), outpoint = concatBytes(toSpendTxid(address, message), u32(0));
if (address.version === 0) {
const code = concatBytes(Uint8Array.of(0x19, 0x76, 0xa9, 0x14), address.program, Uint8Array.of(0x88, 0xac));
return dsha(concatBytes(u32(0), dsha(outpoint), dsha(u32(0)), outpoint, code, ZERO, u32(0), dsha(OUTPUTS), u32(0), u32(1)));
}
return tagged('TapSighash', Uint8Array.of(0, type), u32(0), u32(0), sha256(outpoint), sha256(ZERO), sha256(sized(script)),
sha256(u32(0)), sha256(OUTPUTS), Uint8Array.of(0), u32(0));
}
const encodeWitness = items => base64.encode(concatBytes(Uint8Array.of(items.length), ...items.map(sized)));
function decodeWitness(text) {
try {
const b = base64.decode(text);
let i = 0;
const size = () => {
if (i >= b.length) throw new Error('short');
const n = b[i++];
if (n < 0xfd) return n;
if (n > 0xfd || i + 2 > b.length) throw new Error('size');
i += 2; return b[i - 2] | (b[i - 1] << 8);
};
const items = Array.from({ length: size() }, () => { const n = size(); if (i + n > b.length) throw new Error('short'); i += n; return b.slice(i - n, i); });
return i === b.length ? items : null;
} catch { return null; }
}
export function signBip322(text, message, secret) {
const address = decodeAddress(text);
if (isTaproot(address)) return encodeWitness([schnorr.sign(sighash(address, message), secret, AUX)]);
if (!isP2wpkh(address)) throw new Error('BIP-322 here signs for Taproot and P2WPKH addresses only.');
const der = secp256k1.sign(sighash(address, message), secret).toDERRawBytes();
return encodeWitness([concatBytes(der, Uint8Array.of(1)), secp256k1.getPublicKey(secret, true)]);
}
export function verifyBip322(text, message, proof) {
const address = decodeAddress(text), w = typeof proof === 'string' ? decodeWitness(proof) : null;
if (!address || !w) return false;
try {
if (isTaproot(address)) {
const [sig] = w;
if (w.length !== 1 || !(sig.length === 64 || (sig.length === 65 && sig[64] === 1))) return false;
return schnorr.verify(sig.slice(0, 64), sighash(address, message, sig.length === 65 ? 1 : 0), address.program);
}
if (isP2wpkh(address)) {
const [sig, pubkey] = w;
if (w.length !== 2 || pubkey.length !== 33 || sig.at(-1) !== 1 || !same(ripemd160(sha256(pubkey)), address.program)) return false;
return secp256k1.verify(secp256k1.Signature.fromDER(sig.slice(0, -1)), sighash(address, message), pubkey);
}
} catch { /* a malformed signature or key */ }
return false;
}
// EIP-191 personal_sign: Keccak-256 of "\x19Ethereum Signed Message:\n", the message's byte length and the message;
// the signature is r, s and v (27 or 28), as 0x and 130 hex digits. It checks out when it recovers the address.
export const eip191Hash = message => { const m = utf8ToBytes(message); return keccak_256(concatBytes(utf8ToBytes(`\x19Ethereum Signed Message:\n${m.length}`), m)); };
export const isEthAddress = a => typeof a === 'string' && /^0x[0-9a-fA-F]{40}$/.test(a) && checksumAddress(a.slice(2)) === a;
export function signEip191(message, secret) {
const sig = secp256k1.sign(eip191Hash(message), secret);
return `0x${bytesToHex(sig.toCompactRawBytes())}${(27 + sig.recovery).toString(16)}`;
}
export function verifyEip191(address, message, proof) {
if (!isEthAddress(address) || typeof proof !== 'string' || !/^0x[0-9a-f]{130}$/.test(proof)) return false;
try {
const b = hexToBytes(proof.slice(2)), v = b[64] - 27;
if (v !== 0 && v !== 1) return false;
const key = secp256k1.Signature.fromCompact(b.slice(0, 64)).addRecoveryBit(v).recoverPublicKey(eip191Hash(message));
return ethAddressOf(key.toRawBytes(false)) === address;
} catch { return false; }
}
// NIP-01: an event's id is the SHA-256 of [0, pubkey, created_at, kind, tags, content], and its signature is BIP-340
// over the id. Nostr signers sign events, not bare messages, so the proof signs a kind-1 note whose content is the
// statement, at created_at 0 and with no tags: the key and the statement fix the event, and only its signature is kept.
export const nostrEvent = (pubkey, message) => ({ pubkey, created_at: 0, kind: 1, tags: [], content: message });
const nostrId = (pubkey, message) => sha256(utf8ToBytes(JSON.stringify([0, pubkey, 0, 1, [], message])));
export function npubKey(npub) {
try {
const { prefix, words } = bech32.decode(npub, 90), key = bech32.fromWords(words);
return prefix === 'npub' && key.length === 32 && npub === npub.toLowerCase() ? bytesToHex(Uint8Array.from(key)) : null;
} catch { return null; }
}
export const signNostr = (message, secret) => bytesToHex(schnorr.sign(nostrId(bytesToHex(schnorr.getPublicKey(secret)), message), secret, AUX));
export function verifyNostr(npub, message, proof) {
const key = npubKey(npub);
try { return !!key && typeof proof === 'string' && /^[0-9a-f]{128}$/.test(proof) && schnorr.verify(proof, nostrId(key, message), key); } catch { return false; }
}
const SIGN = { taproot: signBip322, segwit: signBip322, nostr: (key, text, secret) => signNostr(text, secret), ethereum: (key, text, secret) => signEip191(text, secret) };
const VERIFY = { taproot: verifyBip322, segwit: verifyBip322, nostr: verifyNostr, ethereum: verifyEip191 };
// Why a keyring's keys are malformed ('' when each is the right kind of key).
function shape(m) {
const taproot = decodeAddress(m.taproot), segwit = decodeAddress(m.segwit);
if (!isTaproot(taproot)) return 'taproot must be a Taproot address';
if (!isP2wpkh(segwit)) return 'segwit must be a P2WPKH address';
if (taproot.network !== segwit.network) return 'its Taproot and SegWit addresses must be on one network';
if (!npubKey(m.nostr)) return 'nostr must be an npub';
if (!isEthAddress(m.ethereum)) return 'ethereum must be an EIP-55 address';
return '';
}
// A node's keyring as metadata fields: its four keys, and each key's signature of the statement. secrets holds the
// private keys (hex) that sign for them.
export function signKeyring(node, keys, secrets) {
const why = shape(keys);
if (why) throw new Error(`${why[0].toUpperCase()}${why.slice(1)}.`);
const text = statement(node, keys);
return { ...Object.fromEntries(KEYS.map(k => [k, keys[k]])), proof: Object.fromEntries(KEYS.map(k => [k, SIGN[k](keys[k], text, secrets[k])])) };
}
// Why a keyring for this node fails ('' when every key is well formed and signed the statement). Each distinct keyring
// is checked once, since the reducer folds the same events over and over.
const checked = new Map();
export function keyringWhy(node, m) {
const proof = m.proof && typeof m.proof === 'object' && !Array.isArray(m.proof) ? m.proof : {};
const id = JSON.stringify([node, ...KEYS.map(k => [m[k] ?? null, proof[k] ?? null])]);
if (!checked.has(id)) {
if (checked.size >= 1000) checked.clear();
let why = shape(m);
if (!why) {
const text = statement(node, m), bad = KEYS.find(k => !VERIFY[k](m[k], text, proof[k]));
if (bad) why = `the ${bad} key did not sign this keyring`;
}
checked.set(id, why);
}
return checked.get(id);
}
// The slots a keyring's keys bind, one per key whatever its network or letter case: no key binds to two nodes at once.
export const slots = m => KEYS.map(k => `${k}:${k === 'nostr' ? npubKey(m[k]) : k === 'ethereum' ? m[k].toLowerCase() : bytesToHex(decodeAddress(m[k]).program)}`);
+18 -4
View File
@@ -8,7 +8,7 @@ import { Address, Script, Tap } from '@cmdcode/tapscript';
import { bech32, createBase58check } from '@scure/base';
import { sha256 } from '@noble/hashes/sha2';
import { concatBytes, hexToBytes } from '@noble/hashes/utils';
import { PATHS, childMnemonic, nostrKey, nostrSecret, seedBytes, segwitPubkey, segwitSecret, walletSecret } from '../adapters/lineage-keys.js';
import { PATHS, childMnemonic, ethAddress, ethSecret, nostrKey, nostrSecret, seedBytes, segwitPubkey, segwitSecret, walletSecret } from '../adapters/lineage-keys.js';
import { holderCoin } from '../adapters/handoff.js';
import { TestWallet } from '../adapters/wallet.js';
import { agentLeaves, agentRecords, agentSecrets, nodeLeaves, nodeRecords, nodeSecrets } from './leaves.js';
@@ -57,8 +57,9 @@ export function keyOf(keys, state, node) {
}
// 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), the tapkey is its npub as a Taproot
// address (its nostr key with the BIP-86 tweak, so the npub alone finds it), and the npub is its nostr name.
// inscriptions, SegWit takes payments, the tapkey is its npub as a Taproot address (its nostr key with the BIP-86
// tweak, so the npub alone finds it), the npub is its nostr name, and Ethereum is its address there (an org's, at
// account 0, is its legal-control key, as on the lineage key tree page).
export function addressesOf(keys, state, node) {
const seed = seedOf(keys, state, node);
if (!seed) return null;
@@ -68,6 +69,18 @@ export function addressesOf(keys, state, node) {
segwit: { address: Address.p2wpkh.fromPubKey(segwit, 'regtest'), path: PATHS.segwit(seed.account) },
tapkey: { address: Address.p2tr.fromPubKey(tapkeyOf(k.pubkey), 'regtest'), path: PATHS.nostr(seed.account) },
npub: { address: k.npub, path: PATHS.nostr(seed.account) },
ethereum: { address: ethAddress(seed.mnemonic, seed.account), path: PATHS.ethereum(seed.account) },
};
}
// What a node's keyring lists (./keyring.js), and the private keys that sign it: the key that holds its inscription
// (Taproot), its payments key (SegWit), its npub and its Ethereum key.
export function ringOf(keys, state, node) {
const seed = seedOf(keys, state, node);
if (!seed) return null;
const k = keyOf(keys, state, node), a = addressesOf(keys, state, node), { mnemonic, account } = seed;
return {
keys: { taproot: a.taproot.address, segwit: a.segwit.address, nostr: a.npub.address, ethereum: a.ethereum.address },
secrets: { taproot: k.wallet.secret, segwit: segwitSecret(mnemonic, account), nostr: nostrSecret(mnemonic, account), ethereum: ethSecret(mnemonic, account) },
};
}
@@ -143,7 +156,8 @@ export function secretsOf(keys, state, node) {
return {
seed: agent ? null : { from: [seed.origin, ...seed.steps].join(' › '), mnemonic: seed.mnemonic },
account: [['nsec', 'NIP-06', nsecOf(nostr)], ['taproot', 'descriptor', descsum(taproot)],
['segwit', 'descriptor', descsum(`wpkh(${wif(segwitSecret(seed.mnemonic, seed.account))})`)], ['tapkey', 'descriptor', descsum(`tr(${wif(nostr)})`)]],
['segwit', 'descriptor', descsum(`wpkh(${wif(segwitSecret(seed.mnemonic, seed.account))})`)], ['tapkey', 'descriptor', descsum(`tr(${wif(nostr)})`)],
['ethereum', 'private key', `0x${ethSecret(seed.mnemonic, seed.account)}`]],
leaves: agent ? agentSecrets(nostr, `${node.name}@${domain}`) : nodeSecrets(seedBytes(seed.mnemonic), domain),
};
}
+30 -4
View File
@@ -1,6 +1,8 @@
// Orgchain's one interpretation: lineage/v0 events, folded into trees.
// MetadataIndex keeps inscriptions raw; this is a trusted, bundled reducer (no eval, no plugins).
// Rule: an event's parent inscription is the node that authorizes it. The chain enforces that by spending it.
import { KEYS, keyringWhy, slots } from './keyring.js';
export const PROTOCOL = 'lineage/v0';
export const BELTS = ['white', 'green', 'black'];
export const NAME_RULE = 'names are 1-40 lowercase letters, digits or hyphens (they become domains)';
@@ -19,8 +21,12 @@ export const lineageEvents = index => index.events.filter(e => e.protocol === PR
// Fold events in chain order (blocks, then mempool). upTo: the last beat to include. pending: include cued events.
// Items are what nodes make: each is an inscription its maker holds at first. Who holds one later is the chain's to say.
export function reduce(events, { upTo = Infinity, pending = false } = {}) {
const nodes = new Map(), roots = [], log = [], items = new Map();
const nodes = new Map(), roots = [], log = [], items = new Map(), bound = new Map();
const active = n => n.status === 'active';
// A node's current keyring binds its keys (keyring.js slots), so no other node can list them. A newer keyring replaces
// it. A graduation retires it, since the node's keys change with its root, and so does a close.
const release = n => { if (n.keyring && !n.keyring.retired) for (const s of slots(n.keyring.keys)) bound.delete(s); };
const retire = (n, cause, by, beat) => { if (n.keyring && !n.keyring.retired) { release(n); n.keyring.retired = { cause, by, beat }; } };
for (const e of events) {
const confirmed = e.status === 'confirmed';
if (confirmed ? e.height > upTo : !pending) continue;
@@ -89,6 +95,9 @@ export function reduce(events, { upTo = Infinity, pending = false } = {}) {
const from = of.root, reroot = n => { n.root = of.id; for (const id of n.children) if (nodes.get(id).root === from) reroot(nodes.get(id)); };
of.edge = 'adopted'; of.graduated = when; entry.node = of.id;
reroot(of); roots.push(of);
// Its keys change with its root, and so do those of its derived descendants: their keyrings retire.
const rekey = n => { retire(n, 'graduate', of.id, when); for (const id of n.children) if (nodes.get(id).edge === 'derived') rekey(nodes.get(id)); };
rekey(of);
}
break;
}
@@ -98,12 +107,28 @@ export function reduce(events, { upTo = Infinity, pending = false } = {}) {
else {
// Derived children close with their parent; adopted (graduated) children hold their own roots and stay open.
const shut = n => {
n.status = 'closed'; n.closed = when;
n.status = 'closed'; n.closed = when; retire(n, 'close', by.id, when);
for (const id of n.children) { const c = nodes.get(id); if (c.edge === 'derived' && active(c)) { entry.also.push(c.id); shut(c); } }
};
shut(by); entry.node = by.id;
}
break;
case 'keyring': {
if ((why = authority('node'))) fail(why);
else if ((why = keyringWhy(by.id, m))) fail(why);
else {
const keys = Object.fromEntries(KEYS.map(k => [k, m[k]])), own = slots(keys);
const taken = own.findIndex(s => bound.has(s) && bound.get(s) !== by.id);
if (taken >= 0) fail(`its ${KEYS[taken]} key is already bound to ${nodes.get(bound.get(own[taken])).name}`);
else {
release(by);
by.keyring = { id: e.id, beat: when, keys, retired: null };
for (const s of own) bound.set(s, by.id);
entry.node = by.id;
}
}
break;
}
default: fail('not a lineage/v0 op');
}
// root: the holding that acted (a founding's own). roots: every holding the event touches, such as a belt awarded
@@ -113,13 +138,13 @@ export function reduce(events, { upTo = Infinity, pending = false } = {}) {
entry.roots = [...new Set([entry.root, subject].filter(Boolean))];
log.push(entry);
}
return { nodes, roots, log, items };
return { nodes, roots, log, items, bound };
}
function make(e, name, parent, kind) {
return { id: e.id, name, kind, parent: parent?.id ?? null, root: parent ? parent.root : e.id, edge: parent ? 'derived' : null,
status: 'active', index: null, born: e.status === 'confirmed' ? e.height : 'cued',
children: [], badges: [], decisions: [], made: [], graduated: null, closed: null };
children: [], badges: [], decisions: [], made: [], keyring: null, graduated: null, closed: null };
}
// Depth-first rows for one holding, drawn like docs/lineage.md: agents, then orgs, under every node. A child that
@@ -168,6 +193,7 @@ export function summary(entry, state) {
case 'award': return `award ${m.skill} · ${m.belt} → ${name(m.to)}, from ${name(entry.parent)}`;
case 'decide': return `decide “${text(m.title, 140)}” · ${name(entry.parent)}`;
case 'make': return `make ${m.name} · ${name(entry.parent)}`;
case 'keyring': return `keyring · ${name(entry.parent)}`;
case 'graduate': return `graduate ${name(m.of)} from ${name(entry.parent)}`;
case 'close': return `close ${name(entry.parent)}${entry.also.length ? ` and ${entry.also.length} derived` : ''}`;
default: return entry.op;
+1 -1
View File
@@ -1 +1 @@
{"from":120,"ord":{"/blockheight":{"status":200,"body":"131"},"/inscriptions/block/120/0":{"status":200,"body":"{\"ids\":[],\"more\":false,\"page_index\":0}"},"/inscriptions/block/121/0":{"status":200,"body":"{\"ids\":[],\"more\":false,\"page_index\":0}"},"/inscriptions/block/122/0":{"status":200,"body":"{\"ids\":[],\"more\":false,\"page_index\":0}"},"/inscriptions/block/123/0":{"status":200,"body":"{\"ids\":[\"ee6cabd275c53011248727e064c0be3841799b7391bc337520d9aac1ebdc60d1i0\"],\"more\":false,\"page_index\":0}"},"/inscription/ee6cabd275c53011248727e064c0be3841799b7391bc337520d9aac1ebdc60d1i0":{"status":200,"body":"{\"address\":\"bcrt1pmhndmcwyd3qt3lzq7x2fyxa62r8fpj9rn4gr05un6fxt35ft2uzs3tdaf7\",\"charms\":[],\"child_count\":3,\"children\":[\"ac1d4d0cfaeb19d99bc4c6e829d64100b598f2a3cfa538348f230ae3fe09e685i0\",\"4375d999401ea1996d031bddfc2192170e2f8519ada1ff2a5bc77a5eb3b8a514i0\",\"c7d79e81fb104160f3843649d958ab9f56d49df4390fb02b93828021d8dd34dci0\"],\"content_length\":17,\"content_type\":\"text/plain;charset=utf-8\",\"effective_content_type\":\"text/plain;charset=utf-8\",\"fee\":500,\"height\":123,\"id\":\"ee6cabd275c53011248727e064c0be3841799b7391bc337520d9aac1ebdc60d1i0\",\"metaprotocol\":\"lineage/v0\",\"next\":\"ac1d4d0cfaeb19d99bc4c6e829d64100b598f2a3cfa538348f230ae3fe09e685i0\",\"number\":0,\"parents\":[],\"previous\":null,\"properties\":{\"gallery\":[],\"attributes\":{\"title\":null,\"traits\":{}}},\"rune\":null,\"sat\":null,\"satpoint\":\"c7d79e81fb104160f3843649d958ab9f56d49df4390fb02b93828021d8dd34dc:0:0\",\"timestamp\":1792054032,\"value\":1000}"},"/r/metadata/ee6cabd275c53011248727e064c0be3841799b7391bc337520d9aac1ebdc60d1i0":{"status":200,"body":"\"a2626f7065666f756e64646e616d656b796f6c6f7072656e657572\""},"/inscriptions/block/124/0":{"status":200,"body":"{\"ids\":[\"ac1d4d0cfaeb19d99bc4c6e829d64100b598f2a3cfa538348f230ae3fe09e685i0\",\"4375d999401ea1996d031bddfc2192170e2f8519ada1ff2a5bc77a5eb3b8a514i0\"],\"more\":false,\"page_index\":0}"},"/inscription/ac1d4d0cfaeb19d99bc4c6e829d64100b598f2a3cfa538348f230ae3fe09e685i0":{"status":200,"body":"{\"address\":\"bcrt1p436wl2jp50ceq825lm0em2mtvyafeqvf09ggsa9ku6z9rusqat7q40jzp5\",\"charms\":[\"vindicated\"],\"child_count\":4,\"children\":[\"e29b26c17d48c09e72109ae42a833fb24e000cf89790ebbaebea6d3a884fc1bdi0\",\"3d9b514edc1956d5f4fef3edec7267c4920ce81449b8c239e317a53fe0a81885i0\",\"1c8f2b1f187d04c6fd68cb8d2075739253a0ec8642cf6a21e6dfa89a04c61f45i0\",\"e31369d6146a043c538280f0db13d8301a49269835a78ef5dd1f74fbd3769ffei0\"],\"content_length\":16,\"content_type\":\"text/plain;charset=utf-8\",\"effective_content_type\":\"text/plain;charset=utf-8\",\"fee\":500,\"height\":124,\"id\":\"ac1d4d0cfaeb19d99bc4c6e829d64100b598f2a3cfa538348f230ae3fe09e685i0\",\"metaprotocol\":\"lineage/v0\",\"next\":\"4375d999401ea1996d031bddfc2192170e2f8519ada1ff2a5bc77a5eb3b8a514i0\",\"number\":1,\"parents\":[\"ee6cabd275c53011248727e064c0be3841799b7391bc337520d9aac1ebdc60d1i0\"],\"previous\":\"ee6cabd275c53011248727e064c0be3841799b7391bc337520d9aac1ebdc60d1i0\",\"properties\":{\"gallery\":[],\"attributes\":{\"title\":null,\"traits\":{}}},\"rune\":null,\"sat\":null,\"satpoint\":\"e31369d6146a043c538280f0db13d8301a49269835a78ef5dd1f74fbd3769ffe:0:0\",\"timestamp\":1792054033,\"value\":1000}"},"/r/metadata/ac1d4d0cfaeb19d99bc4c6e829d64100b598f2a3cfa538348f230ae3fe09e685i0":{"status":200,"body":"\"a3626f70656269727468646e616d656a77656c6c2d6b6e6f776e646b696e64636f7267\""},"/inscription/4375d999401ea1996d031bddfc2192170e2f8519ada1ff2a5bc77a5eb3b8a514i0":{"status":200,"body":"{\"address\":\"bcrt1ps07srr40g8v2t4e9larkeecct56xj67ue5ead0tuq50lj8888cms3d2xx6\",\"charms\":[\"vindicated\"],\"child_count\":9,\"children\":[\"82d9ba7ae7dd4252fd5bae99d04c70feafcc4f93b0e716e8f8738f914bbea1f6i0\",\"97cd35c5cb52333b987dcea052cd82a7bc9c64a5d2a5fb8c831b42fa63565c8fi0\",\"9414b00451ba60700c236e36c4ead1b86e73d9a76b1e96a46e2f851cc07c6e5fi0\",\"1b48e841f2dbee9cd49a59872ba3ad880d2498c29fed351f54870379982bd10ai0\"],\"content_length\":10,\"content_type\":\"text/plain;charset=utf-8\",\"effective_content_type\":\"text/plain;charset=utf-8\",\"fee\":500,\"height\":124,\"id\":\"4375d999401ea1996d031bddfc2192170e2f8519ada1ff2a5bc77a5eb3b8a514i0\",\"metaprotocol\":\"lineage/v0\",\"next\":\"82d9ba7ae7dd4252fd5bae99d04c70feafcc4f93b0e716e8f8738f914bbea1f6i0\",\"number\":2,\"parents\":[\"ee6cabd275c53011248727e064c0be3841799b7391bc337520d9aac1ebdc60d1i0\"],\"previous\":\"ac1d4d0cfaeb19d99bc4c6e829d64100b598f2a3cfa538348f230ae3fe09e685i0\",\"properties\":{\"gallery\":[],\"attributes\":{\"title\":null,\"traits\":{}}},\"rune\":null,\"sat\":null,\"satpoint\":\"5eaebae86b7957b427121d471de2ff14099467eaa16fded137f7b641ca55788f:0:0\",\"timestamp\":1792054033,\"value\":1000}"},"/r/metadata/4375d999401ea1996d031bddfc2192170e2f8519ada1ff2a5bc77a5eb3b8a514i0":{"status":200,"body":"\"a3626f70656269727468646e616d656474696d65646b696e64636f7267\""},"/inscriptions/block/125/0":{"status":200,"body":"{\"ids\":[\"82d9ba7ae7dd4252fd5bae99Line truncated
{"from":120,"ord":{"/blockheight":{"status":200,"body":"130"},"/inscriptions/block/120/0":{"status":200,"body":"{\"ids\":[],\"more\":false,\"page_index\":0}"},"/inscriptions/block/121/0":{"status":200,"body":"{\"ids\":[],\"more\":false,\"page_index\":0}"},"/inscriptions/block/122/0":{"status":200,"body":"{\"ids\":[\"670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0\"],\"more\":false,\"page_index\":0}"},"/inscription/670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0":{"status":200,"body":"{\"address\":\"bcrt1p98e82vaue2cf0t3dh7ew6rtkvwfmfthgkpquqpvuky2upyac3a3szcnws7\",\"charms\":[],\"child_count\":4,\"children\":[\"9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0\",\"960288659f234616573724e2a4ef1b62435b404a8d7a82ab580b9340262acdc4i0\",\"3323322d47592ab9977d8a3c8cbf7894581d287b045497074683046a5af74f94i0\",\"7038f2054a1019fcf4b14a099c101b55c350ae9c256bdf2700e1fa4ccfaa7b2ei0\"],\"content_length\":17,\"content_type\":\"text/plain;charset=utf-8\",\"effective_content_type\":\"text/plain;charset=utf-8\",\"fee\":500,\"height\":122,\"id\":\"670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0\",\"metaprotocol\":\"lineage/v0\",\"next\":\"9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0\",\"number\":0,\"parents\":[],\"previous\":null,\"properties\":{\"gallery\":[],\"attributes\":{\"title\":null,\"traits\":{}}},\"rune\":null,\"sat\":null,\"satpoint\":\"7038f2054a1019fcf4b14a099c101b55c350ae9c256bdf2700e1fa4ccfaa7b2e:0:0\",\"timestamp\":1792059012,\"value\":1000}"},"/r/metadata/670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0":{"status":200,"body":"\"a2626f7065666f756e64646e616d656b796f6c6f7072656e657572\""},"/inscriptions/block/123/0":{"status":200,"body":"{\"ids\":[\"9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0\",\"960288659f234616573724e2a4ef1b62435b404a8d7a82ab580b9340262acdc4i0\",\"3323322d47592ab9977d8a3c8cbf7894581d287b045497074683046a5af74f94i0\"],\"more\":false,\"page_index\":0}"},"/inscription/9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0":{"status":200,"body":"{\"address\":\"bcrt1pfu5jhcgwcl5ulxcedvawvfwvazr6yg47c7atwdeg4pgjlaz5y6xsmnpl8p\",\"charms\":[\"vindicated\"],\"child_count\":4,\"children\":[\"4cb00b54088a273ba4898a087f89f691cb5588788a88fbf3803548cabef9df5bi0\",\"039b50131216fb534fba55eb5303879d363fa5e79d5b9301593cbfd0a8ba5cc1i0\",\"af00a5c03ae36ce58514f3913c96278d19069b80b15596234d7269ae9df5da1bi0\",\"d685b1aa792fa8374446810022893b83e81e94d751db4dfaff933ee940e87f6di0\"],\"content_length\":16,\"content_type\":\"text/plain;charset=utf-8\",\"effective_content_type\":\"text/plain;charset=utf-8\",\"fee\":500,\"height\":123,\"id\":\"9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0\",\"metaprotocol\":\"lineage/v0\",\"next\":\"960288659f234616573724e2a4ef1b62435b404a8d7a82ab580b9340262acdc4i0\",\"number\":1,\"parents\":[\"670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0\"],\"previous\":\"670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0\",\"properties\":{\"gallery\":[],\"attributes\":{\"title\":null,\"traits\":{}}},\"rune\":null,\"sat\":null,\"satpoint\":\"d685b1aa792fa8374446810022893b83e81e94d751db4dfaff933ee940e87f6d:0:0\",\"timestamp\":1792059012,\"value\":1000}"},"/r/metadata/9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0":{"status":200,"body":"\"a3626f70656269727468646e616d656a77656c6c2d6b6e6f776e646b696e64636f7267\""},"/inscription/960288659f234616573724e2a4ef1b62435b404a8d7a82ab580b9340262acdc4i0":{"status":200,"body":"{\"address\":\"bcrt1pc4kjexj5dj6v0eqssyxhvp7gharjz57hasjt08e3c8tfjrzmqc0qudw520\",\"charms\":[\"vindicated\"],\"child_count\":9,\"children\":[\"3ccebeb504beda9f8b8ce6f18486476fd6e663d645d6341dec53235b5ff5775fi0\",\"404acff09740f6ff639788419d41f08377ef8373f517d5b92a74690e68b47ffdi0\",\"7f19f0b1aaa45c08f1a0fcd85ea0201ee27335be351f985c3c8c1325212858abi0\",\"4c156019dfc5faa5b0f4bdb5693141560d7fb64c0e253bcdb45dee21093536cbi0\"],\"content_length\":10,\"content_type\":\"text/plain;charset=utf-8\",\"effective_content_type\":\"text/plain;charset=utf-8\",\"fee\":500,\"height\":123,\"id\":\"960288659f234616573724e2a4ef1b62435b404a8d7a82ab580b9340262acdc4i0\",\"metaprotocol\":\"lineage/v0\",\"next\":\"3323322d47592ab9977d8a3c8cbf7894581d287b045497074683046a5af74f94i0\",\"number\":2,\"parents\":[\"670e71443c12661e35141b76a4d387321bb3c29212db7b04c542026ee6887cfdi0\"],\"previous\":\"9f02e5595eb7c2eb0f812e352508c0b417968e889cc4ccbec5a45f9db4540335i0\",\"properties\":{\"gallery\":[],\"attributes\":{\"title\":null,\"traits\":{}}},\"rune\":null,\"sat\":null,\"satpoint\":\"7f54b915ca84d5ff088a457e14a8075a332e2f2df8e4e04d4e34c0016fedbb66:0:0\",\"timestamp\":1792059012,\"value\":1000}"},"/r/metadata/960288659f234616573724e2a4ef1b62435b404a8d7a82ab580b9340262acdc4i0":{"status":200,"body":"\"a3626f70656269727468646e616d656474696d65646b696e64636f7267\""},"/inscription/3323322d47592ab9977d8a3c8cbLine truncated
+7 -1
View File
@@ -20,7 +20,7 @@ const hex = script => Script.encode(script, false).hex;
test('real ord indexed the regtest rehearsal, and the same reducer folds it into the trees the lab drew', async () => {
const events = await ordEvents(ordClient('http://ord.test', replay), { from: fixture.from });
assert.equal(events.length, 21);
assert.equal(events.length, 26);
assert.ok(events.every(e => e.protocol === 'lineage/v0' && e.parents <= 1 && /^[0-9a-f]{64}:\d+$/.test(e.location)));
const s = ordState(events), id = name => [...s.nodes.values()].find(n => n.name === name).id;
assert.ok(s.log.every(e => e.ok));
@@ -31,6 +31,12 @@ test('real ord indexed the regtest rehearsal, and the same reducer folds it into
assert.deepEqual(census(s, id('lineage')), { orgs: 0, agents: 0, belts: 0, decisions: 1, closed: 0 });
assert.deepEqual(s.nodes.get(id('scout')).badges.map(b => [b.skill, b.belt, s.nodes.get(b.by).name]), [['listen', 'white', 'well-known']]);
assert.deepEqual([...s.items.values()].map(i => [i.name, s.nodes.get(i.maker).name]), [['launch-kit', 'sol']]);
// Five keyrings, their proofs checked from ord's metadata (two 520-byte pushes each): lineage's first retired when it
// graduated, and the one it published a block later stands. Four keyrings, four keys each, are bound.
assert.deepEqual(s.log.filter(e => e.op === 'keyring').map(e => s.nodes.get(e.node).name), ['yolopreneur', 'sol', 'lineage', 'lineage', 'scout']);
assert.deepEqual([...s.nodes.values()].filter(n => n.keyring).map(n => [n.name, n.keyring.retired]).sort(),
[['lineage', null], ['scout', null], ['sol', null], ['yolopreneur', null]]);
assert.equal(s.bound.size, 16);
// Byte for byte: the same events in the same beats, the same metadata and parents, every inscription in the same
// output (the launch kit where lineage's deal put it). Only the miner's order within blocks differs.
const { problems, reordered } = compareWithLab({ chain: new LocalChain(fixture.chain) }, events);
+170
View File
@@ -0,0 +1,170 @@
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');
});
+5 -3
View File
@@ -8,7 +8,7 @@ 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, nostrKey, nostrSecret, seedBytes, segwitPubkey, segwitSecret, walletSecret } from '../src/adapters/lineage-keys.js';
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';
@@ -118,6 +118,7 @@ test('each holding has its own root, and every node holds its inscription at its
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/);
@@ -149,8 +150,9 @@ test('secrets: an org shows its seed, a seat only its account, and every descrip
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']);
assert.deepEqual(seat.account.slice(1).map(([, , d]) => d), [descsum(`tr(${wif(walletSecret(opsSeed, 1))})`),
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'));
+3
View File
@@ -89,6 +89,9 @@ test('the yolopreneur sample is six beats of the drawing in docs/lineage.md', ()
assert.equal(named('lineage').edge, 'adopted');
assert.deepEqual(s.roots.map(r => r.name), ['yolopreneur', 'lineage']); // graduated: a holding of its own
assert.deepEqual(named('sol').badges.map(b => `${b.skill}:${b.belt}`), ['launch:green']);
// Three keyrings; lineage's retires when it graduates, since its keys change with its root.
assert.deepEqual(['yolopreneur', 'sol', 'lineage'].map(x => [named(x).keyring.beat, named(x).keyring.retired?.cause ?? null]),
[[3, null], [6, null], [6, 'graduate']]);
assert.deepEqual(named('time').children.map(id => s.nodes.get(id).name), ['luna', 'sol', 'astra', 'docscope', 'dollhouse', 'doodleloop', 'hearthframe']);
assert.deepEqual([2, 3, 4, 5].map(upTo => state(lab, { upTo }).nodes.size), [1, 3, 8, 12]);
assert.throws(() => seedSample(lab), /already named yolopreneur/);
+5 -4
View File
@@ -160,12 +160,13 @@ test('graduation moves money to the new keys, and the graduate pays its own clai
const lab = emptyLab();
seedSample(lab);
const { now, log, live } = money(lab);
// lineage graduates at beat 7 into a holding of its own, so it mines half of that beat
// lineage graduates at beat 7 into a holding of its own, so it mines half of that beat. Its keyring at beat 6 filled
// its old gas tank, which stays with its old keys; its acts after graduating fill a new one.
assert.deepEqual(now, { yolopreneur: 275 * SAT - 50 * SAT - 2 * FEE - TOPUP, 'well-known': 8 * SAT - FEE - TOPUP,
lineage: 27 * SAT - FEE - TOPUP, time: 38.5 * SAT - 2 * FEE - 3 * TOPUP, sol: SAT - TOPUP, luna: 0.5 * SAT });
assert.deepEqual(log.slice(-2), [['move', 7, 'lineage', 'lineage', 2 * SAT - FEE], ['gas', 7, 'lineage', 'lineage', 10_000]]);
lineage: 27 * SAT - FEE - 2 * TOPUP, time: 38.5 * SAT - 2 * FEE - 3 * TOPUP, sol: SAT - TOPUP, luna: 0.5 * SAT });
assert.deepEqual(log.slice(-2), [['move', 7, 'lineage', 'lineage', 2 * SAT - TOPUP - FEE], ['gas', 7, 'lineage', 'lineage', 10_000]]);
const lineage = [...live.nodes.values()].find(n => n.name === 'lineage');
assert.equal(lab.chain.balance(payerOf(lab.keys, live, lineage).script), 27 * SAT - FEE - TOPUP);
assert.equal(lab.chain.balance(payerOf(lab.keys, live, lineage).script), 27 * SAT - FEE - 2 * TOPUP);
assert.equal(money(lab, 5).now.lineage, 2 * SAT); // before graduating, at its derived address
const solo = emptyLab();
const root = act(solo, 'found', null, { name: 'acme' }), kid = act(solo, 'birth', root, { name: 'kid' });
+25 -1
View File
@@ -206,7 +206,7 @@ test('the sample replays, any beat can be inspected, and the lab survives a relo
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);
await expect(inspect.locator('.code', { hasText: 'Public lines to paste' }).locator('pre')).toContainText(text);
}
await expect(fact('Tapkey')).toContainText('bcrt1p');
await expect(page.locator('#reveal')).toHaveAttribute('aria-expanded', 'false');
@@ -333,3 +333,27 @@ test('an agent pays a registry in another holding, and the registry awards it a
await expect(row(page, 'scout')).toContainText('listen');
expect(problems).toEqual([]);
});
test('a node publishes its keyring; a graduation retires it, and the graduate publishes its new keys', async ({ page }) => {
const problems = await open(page), inspect = page.locator('#inspect-body');
await page.locator('#sample').click();
await expect(say(page)).toContainText('Loaded the yolopreneur sample', { timeout: 20_000 });
await row(page, 'yolopreneur').click();
await expect(inspect).toContainText('published at beat 3');
await expect(inspect).toContainText('Its legal-control key, the one that holds the entity NFT');
await row(page, 'sol').click();
await expect(inspect).toContainText('published at beat 6');
await expect(inspect).toContainText("account 2 of its org's seed, the way Ledger Live numbers Ethereum accounts");
await row(page, 'lineage').click();
await expect(inspect).toContainText('retired at beat 7');
await expect(inspect).toContainText('It graduated, so its keys changed with its new root. Keyring publishes a new one.');
await verb(page, 'keyring');
await expect(page.locator('#keyring-keys')).toContainText('npub1');
await submit(page, 'keyring');
await expect(say(page)).toHaveText('lineage published its keyring, at beat 8: four keys, each signing the same statement.');
await expect(inspect).toContainText('published at beat 8');
await expect(inspect.locator('pre', { hasText: 'lineage/v0 keyring' })).toContainText('ethereum: 0x');
await expect(page.locator('#act-why')).toHaveText("lineage's keyring already lists these keys, since beat 8.");
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
expect(problems).toEqual([]);
});
+3 -2
View File
@@ -1,8 +1,9 @@
# Regtest rehearsal
Lineage is an ordinals metaprotocol (`SPEC.md`). This folder checks that on real Bitcoin Core and real `ord`: the lab's
own action code makes the yolopreneur sample, Bitcoin Core (regtest, standardness on) accepts its transactions byte
for byte, `ord` indexes the inscriptions, and the same reducer folds ord's view back into the same trees.
own action code makes the yolopreneur sample, keyrings included, Bitcoin Core (regtest, standardness on) accepts its
transactions byte for byte, `ord` indexes the inscriptions, and the same reducer folds ord's view back into the same
trees.
Regtest is local and private: nothing is downloaded, blocks are mined on demand, and the data folders stay a few MB.
Test coins only.
+9 -4
View File
@@ -68,7 +68,8 @@ deposit(lab, yolo, 30 * SAT);
deposit(lab, yolo, 30 * SAT);
await beat('found yolopreneur and fund it');
const wk = act(lab, 'birth', yolo, { name: 'well-known', kind: 'org' }), time = act(lab, 'birth', yolo, { name: 'time', kind: 'org' });
await beat('well-known and time');
act(lab, 'keyring', yolo, {});
await beat("well-known and time, and yolopreneur's keyring");
send(lab, yolo, time, 40 * SAT);
send(lab, yolo, wk, 10 * SAT);
act(lab, 'birth', wk, { name: 'mailr', kind: 'org' });
@@ -85,20 +86,24 @@ act(lab, 'award', time, { to: sol, skill: 'launch', belt: 'green' });
act(lab, 'award', time, { to: luna, skill: 'post', belt: 'white' });
act(lab, 'decide', yolo, { title: 'Every venture gets a page', why: '<venture>.yolopreneur.lol, served from one pages branch.' });
const kit = act(lab, 'make', sol, { name: 'launch-kit', note: 'The page, the post and the checklist every venture launches with.' });
await beat('belts, a decision and an item');
act(lab, 'keyring', sol, {});
act(lab, 'keyring', lineage, {}); // retired at the next beat, when lineage graduates and its keys change
await beat('belts, a decision, an item and two keyrings');
act(lab, 'graduate', wk, { of: lineage });
act(lab, 'decide', lineage, { title: 'Lineage gets its own org', why: 'Its own repo on Teahub now, lineage.llc later: adopted, not derived.' });
accept(lab, offer(lab, kit, lineage, 0.1 * SAT).txid);
await beat('lineage graduates and buys the launch kit');
const acme = act(lab, 'found', null, { name: 'acme' });
deposit(lab, acme, 5 * SAT);
await beat('another holding');
act(lab, 'keyring', lineage, {}); // its new keys, a beat after the graduation
await beat("another holding, and lineage's new keyring");
const scout = act(lab, 'birth', acme, { name: 'scout', kind: 'agent' });
send(lab, acme, scout, SAT);
await beat('an agent of acme');
send(lab, scout, wk, 0.1 * SAT);
act(lab, 'award', wk, { to: scout, skill: 'listen', belt: 'white' });
await beat('scout pays well-known, which awards it a belt');
act(lab, 'keyring', scout, {});
await beat("scout pays well-known, which awards it a belt, and scout's keyring");
const live = reduce(lineageEvents(new MetadataIndex(lab.chain)), { pending: true });
for (const root of live.roots) console.log(rows(live, root.id).map(r => (r.type === 'group' ? `${r.guide}${r.label}` : `${r.guide}${r.path} ${r.node.name}`)).join('\n'));