diff --git a/WRITING-PLAN.md b/WRITING-PLAN.md index 4f736921..5a35676b 100644 --- a/WRITING-PLAN.md +++ b/WRITING-PLAN.md @@ -8,6 +8,7 @@ How the manuscript gets produced, verified, reviewed, and kept alive. The table - Diagrams are the longest-lived medium. Every chapter budget includes them: a state machine, and a figure for every walkthrough step. The step figures share one column layout and accumulate into the program's full account map, so no map is drawn up front. - Style is inherited wholesale from the [companion AI plugin](https://github.com/quicknode/solana-finance-claude-plugin): onchain and offchain unhyphenated, "instruction handlers" for the functions and "instructions" for their input, "minor unit" not "base unit", "network" for Solana and "program" for onchain code with "protocol" never used for either, one name per role enforced within each chapter, concrete two-asset examples, real handler names in lifecycle prose, persona-driven walkthroughs. Also from the skill, and deliberately not repeated below: handler naming (`initialize_x()` for setup, the domain's verb otherwise), PDA versus account, mint versus token, example assets, plain language and concrete definitions, emphasis, stating the mechanism rather than gesturing at it, and talking to readers rather than about the document. **A rule that would apply to any Solana finance writing belongs in the skill; this file adds only what is specific to the book.** `scripts/lint-manuscript.mjs` still enforces several of the skill's rules over the manuscript, so a rule can live there and be guarded here. - Banned phrases, enforced by `scripts/lint-manuscript.mjs` (`npm run lint`) over the manuscript files: "the oracle problem", "instrument" (say "financial software" or "program"), the hyphenated chain words, and em-dashes. The lint also bans a group of phrases that read as machine-written, on the same grounds as the em-dash: a reader clocks them before finishing the sentence, and every one of them stands in for a mechanism rather than stating it. "Load-bearing" is the type specimen; say what depends on the thing and what breaks without it. The rest are "delve", "tapestry", "intricate", "multifaceted", "holistic", "myriad", "seamless", "robust", "pivotal", "crucial", "elevate", "testament to", "deep dive", "paradigm shift", "game-changer", "unlock the power", "realm of", "landscape of", "in today's", "it is worth noting", and "underscores the". A second group is banned for announcing that a point exists instead of making it, each one written in this manuscript and each replaced by the content it stood in front of: "what matters", "the point is", "that is the whole reason", and "does the work of". "What matters here is the unit" became "the index advances by slots, not seconds"; delete the announcement and keep what followed it. The same announcement came back wearing a pronoun once the plain forms were banned, so "that is the point", "which is exactly the point", "is the one that matters", "matters more than it looks" and "it is worth being" are banned too. A related group points the reader at the page instead of telling them what is on it: "Note what is pledged", "Notice who did nothing", "Watch K in the story below". Each is one word longer than the sentence that says the thing, and the fix is to say it. Watching a real market or looking away from the chain is the reader doing something in the world, and stays. "Treats" meaning "discusses" ("the *Lending and Borrowing* chapter treats it fully") is banned; say "explains" or "covers". "Treats X as Y" is a different verb and stays. "Pin" is banned in both of its uses here: a test enforces, checks, or asserts, and a value is fixed or recorded at creation. "Capstone" is banned for rating a chapter's place instead of saying what it does; say "this chapter" or name it. "Walks" with a chapter, book, or suite as its subject is banned as tour-guide framing: a chapter explains or builds, and a suite follows or runs a path; a loop that walks a tree or the states of a machine is mechanism and stays. "Works" meaning succeeds or functions stays ("the deadline arithmetic flips which handler works"); what is banned is working an example ("the chapter works an example"), where the chapter shows or builds it instead. A third group rates the writing instead of doing it, and each was found many times over in one audit: "honest" and "honestly" (twenty uses, none about honesty), "quietly" as an intensifier, "the discipline" and "hygiene" as names for a rule, "by now you", and "worth stealing", "worth copying", "worth noticing", "worth printing", "worth studying". The paragraph-closing maxim ("A wall that has never been pushed on is a hypothesis") is the same tell with no regex to catch it, so the companion AI plugin describes it and the lint does not. A fourth group is mannered prose: an idiom, a slang word, or a metaphor standing where a literal phrase of the same length was available, and each was found in one audit: "a single whale" (a single large backer), "config foot-guns" (dangerous configurations), "zombie positions", "cannot be rugged", "by fat-finger", "after the whistle", "mid-flight", "the hot path", "hand-rolled", "comes off the top", "pays for itself", "comes home", "the ladder has three rungs". A metaphor drags in connotations the writer did not choose, so the fix is to say what you mean. The teaching analogy that carries a mechanism stays (a mint is a factory, Solana is one giant computer), and so does the finance term the field uses (a deep pool, seeding a pool, the rails); the lint catches the words above, and the companion AI plugin describes the rule in general. "Fail open" and "fail closed" are banned for a different reason: an open circuit stops and an open door lets everything through, so readers split on which way each one points. Say what the program does instead: it refuses the transaction, or it lets every request through. The lint also rejects negative framing of offchain truth (any "cannot verify the price" phrasing): the manuscript teaches the positive mechanism, checking a fact's provenance, freshness, and confidence against a source the program trusts. "Property test" is banned in favor of "invariant test", the companion AI plugin's rule, because "property" also names an account's fields; the one exception is the testing chapter's single pointer to the libraries' own term, "property-based testing". "Smart contract" is banned in favour of "program", with exactly two exceptions, both contrastive: the Introduction's line telling a reader arriving from another chain what a Solana program is, and the glossary's definition. +- A key pair is two keys, and only its public key is an address: prose never calls a key pair an address or an account, never puts one on or off the curve, and never says a PDA has one. In the figures, an address made from a key pair is an unlabeled `r="6"` dot on the curve with its account's box beside it; it is never an offcurve dot with a caption, and no caption names a key pair. `scripts/lint-manuscript.mjs` fails the prose and the captions, and `scripts/check-figure-geometry.mjs` fails a public-key dot off the curve, a derived-address dot on it, and any dot more than 20 units from an account box. Seven order-book figures once drew the order book and its vaults as offcurve dots captioned `A CLIENT KEYPAIR`. The vaults are now PDAs of the market; the book, which the client must create, has its box above the curve at the top right, beside its dot. - No sentence runs past forty-five words, enforced by `scripts/lint-manuscript.mjs` over prose, list items, and boxouts. The ceiling is deliberately loose, because it is catching a specific failure rather than policing rhythm: every sentence it first caught was a list that had been glued into a paragraph, or two separate claims joined by a colon, and the fix each time was to split at the punctuation already sitting there. The same pass bans a short list of words that have a plain equivalent and carry no meaning of their own here: "utilize", "commence", "prior to", "subsequent to", "in order to", "in the event that", "due to the fact that", "whilst", "amongst", "thus", "hence", "therefore", "approximately", "numerous", and "sufficient". Words that only *look* bureaucratic stay, because they are exact in this domain: a transaction **executes**, a supplier earns **yield**, a maker **provides** liquidity, a program signs by **presenting** seeds, and a firm **operates** a venue. "Confirm" is banned as a loose synonym for "check", because `confirmed` is one of Solana's three commitment levels and the book teaches them. This is the one rule taken from ASD-STE100 Simplified Technical English; the rest of that standard is written for aircraft maintenance procedures, and its bans on direct address, rhetorical questions, and metaphor would flatten a book that teaches with a story. - An address is written as a markdown link, `[host.tld](https://host.tld)`, never as loose prose, because the PDF build only reaches links. It prints in italic, which is what technical publishing does with an address (O'Reilly's house style and No Starch's) and needs no machine face to tell a reader it is copied exactly; the monospace face stays with `program_id`, where it separates code from the prose around it. Never underlined: underlining is a screen convention and collides with descenders in a serif face. The scheme prints, because it is part of the address and because technical publishing prints one: O'Reilly sets a link as "words (https://host/path)". A leading `www.` does not, being a subdomain nobody types and every browser supplies. The link keeps whatever it was written with, and the link is what a click follows, so write the link text as the host and path alone and let the markdown target carry the full address. A link whose text is words rather than an address gets that address printed after it in parentheses, since nobody can click a printed page. Both are done by `scripts/build-pdf.mjs`; the lint catches an address written as prose. A long address may break across lines, at a slash; what may not break is where it points, so `npm run build-pdf` reads the link annotations back out of the finished PDF and fails unless every line of a broken address carries the whole destination. But an address does not belong in running prose at all: it is a string of punctuation inside a justified measure, and a reader holding the book cannot act on it while reading the sentence anyway. It goes in a footnote, which names the page it points at, so the sentence keeps its words and the address waits at the foot of the page for a reader who wants it. Two things stay inline. `solanabook.org` is short and is the thing its sentence is naming, and the Glossary is reference matter already, whose entries would read oddly with notes hanging off them. - American spelling throughout, which the lint guards in two batches: the "-our" and "-ise" endings, and the doubled-l forms and noun/verb splits British English keeps ("canceling", "modeling", "cancelable", "center", "license"). Identifiers keep whatever the companion repository shipped, so the `Cancelled` enum variant is written capitalized and left alone. Typography in the source is one apostrophe and one set of quotes, both straight, and one italic marker, the asterisk. The figures are typed the same way. The build sets the quotes the way a typeset book carries them, ’ and “ ” in prose, headings, and figure labels, and straight inside code, which is O'Reilly's convention and every other technical publisher's; the lint rejects a curly quote, as a character or an entity, in a chapter or a figure, so that grep and the build only ever meet one form. A single quote is always an apostrophe, never a quotation inside a quotation, and the lint holds the manuscript to that too. Underscore italics are not a second house style: an underscore is a word character, so `\b` never fires against the start of `_Arithmetic of Money_`, and a chapter reference written that way reads clean to the cross-reference check. The lint bans them for that reason. diff --git a/building-financial-products-in-solana/figures/order-book-accounts.svg b/building-financial-products-in-solana/figures/order-book-accounts.svg index 7e75a6ec..bb88256c 100644 --- a/building-financial-products-in-solana/figures/order-book-accounts.svg +++ b/building-financial-products-in-solana/figures/order-book-accounts.svg @@ -1,20 +1,19 @@ - + Every account the book touches - Solana Devnet + Solana Devnet + - - - - NVDAx mint - - - - - - USDC mint + + + + order book (≈180 KB) + + asks_root: Alice's ask, 10 @ 265 + bids_root: Bob's bid, 4 @ 250 + next_order_id: 3 @@ -229,19 +228,8 @@ is_active: true - - A CLIENT KEYPAIR - - - order book (≈180 KB) - - asks_root: Alice's ask, 10 @ 265 - bids_root: Bob's bid, 4 @ 250 - next_order_id: 3 - - - A CLIENT KEYPAIR + "BASE_VAULT" + MARKET ADDRESS base vault @@ -251,7 +239,7 @@ - A CLIENT KEYPAIR + "QUOTE_VAULT" + MARKET ADDRESS quote vault @@ -261,7 +249,7 @@ - A CLIENT KEYPAIR + "FEE_VAULT" + MARKET ADDRESS fee vault @@ -271,4 +259,5 @@ the snapshot is both orders resting, before anything matches the vaults hold what each resting order locked + not drawn: the NVDAx and USDC mints, which these accounts name only as seeds and fields diff --git a/building-financial-products-in-solana/figures/order-book-alice-ask.svg b/building-financial-products-in-solana/figures/order-book-alice-ask.svg index 98874a95..3ea083f5 100644 --- a/building-financial-products-in-solana/figures/order-book-alice-ask.svg +++ b/building-financial-products-in-solana/figures/order-book-alice-ask.svg @@ -1,23 +1,20 @@ - + Alice's ask rests - Solana Devnet - - - - - - NVDAx mint - - - - - - - USDC mint + Solana Devnet + + + + + + order book (≈180 KB) + + asks_root: Alice's ask, 10 @ 265 + bids_root: empty + next_order_id: 2 @@ -195,19 +192,8 @@ is_active: true - - A CLIENT KEYPAIR - - - order book (≈180 KB) - - asks_root: Alice's ask, 10 @ 265 - bids_root: empty - next_order_id: 2 - - - A CLIENT KEYPAIR + "BASE_VAULT" + MARKET ADDRESS base vault @@ -217,14 +203,14 @@ - A CLIENT KEYPAIR + "QUOTE_VAULT" + MARKET ADDRESS quote vault - A CLIENT KEYPAIR + "FEE_VAULT" + MARKET ADDRESS fee vault @@ -236,4 +222,5 @@ no bid reaches 265, so nothing matches and the ask becomes a leaf in the ask tree; resting costs only rent and the transaction fee + not drawn: the NVDAx and USDC mints, which these accounts name only as seeds and fields diff --git a/building-financial-products-in-solana/figures/order-book-cancel.svg b/building-financial-products-in-solana/figures/order-book-cancel.svg index 539d219a..02694f09 100644 --- a/building-financial-products-in-solana/figures/order-book-cancel.svg +++ b/building-financial-products-in-solana/figures/order-book-cancel.svg @@ -1,22 +1,19 @@ - + Bob cancels, and the lock returns - Solana Devnet - - - - - - NVDAx mint - - - - - - - USDC mint + Solana Devnet + + + + + + order book (≈180 KB) + + asks_root: Alice's ask, 10 @ 265 + bids_root: empty + next_order_id: 4 @@ -207,34 +204,23 @@ quote_lot_size: 1 is_active: true - - - A CLIENT KEYPAIR - - - order book (≈180 KB) - - asks_root: Alice's ask, 10 @ 265 - bids_root: empty - next_order_id: 4 - - A CLIENT KEYPAIR + "BASE_VAULT" + MARKET ADDRESS base vault - A CLIENT KEYPAIR + "QUOTE_VAULT" + MARKET ADDRESS quote vault - A CLIENT KEYPAIR + "FEE_VAULT" + MARKET ADDRESS fee vault @@ -243,4 +229,5 @@ vault, and only the claim to it moves onto Bob's ledger line, by the same price-times-remaining formula that locked it a Filled or already-Cancelled order rejects with OrderNotCancellable + not drawn: the NVDAx and USDC mints, which these accounts name only as seeds and fields diff --git a/building-financial-products-in-solana/figures/order-book-initialize-market.svg b/building-financial-products-in-solana/figures/order-book-initialize-market.svg index a7ff86e7..07dd0715 100644 --- a/building-financial-products-in-solana/figures/order-book-initialize-market.svg +++ b/building-financial-products-in-solana/figures/order-book-initialize-market.svg @@ -1,20 +1,20 @@ - + Maria opens the NVDAx market - Solana Devnet + Solana Devnet + - - - - NVDAx mint - - - - - - USDC mint + + + + order book (≈180 KB) + + asks_root: empty + bids_root: empty + next_order_id: 1 + NEW @@ -143,20 +143,8 @@ NEW - - A CLIENT KEYPAIR - - - order book (≈180 KB) - - asks_root: empty - bids_root: empty - next_order_id: 1 - NEW - - - A CLIENT KEYPAIR + "BASE_VAULT" + MARKET ADDRESS base vault @@ -167,7 +155,7 @@ - A CLIENT KEYPAIR + "QUOTE_VAULT" + MARKET ADDRESS quote vault @@ -178,7 +166,7 @@ - A CLIENT KEYPAIR + "FEE_VAULT" + MARKET ADDRESS fee vault @@ -191,4 +179,5 @@ 180 KB is more than a program is allowed to allocate the handler validates the sizes and the fee, records every address on the market, and stamps two empty trees on the book + not drawn: the NVDAx and USDC mints, which these accounts name only as seeds and fields diff --git a/building-financial-products-in-solana/figures/order-book-match.svg b/building-financial-products-in-solana/figures/order-book-match.svg index 6e161633..0b1f9923 100644 --- a/building-financial-products-in-solana/figures/order-book-match.svg +++ b/building-financial-products-in-solana/figures/order-book-match.svg @@ -1,23 +1,20 @@ - + Carol's ask fills Bob's bid - Solana Devnet - - - - - - NVDAx mint - - - - - - - USDC mint + Solana Devnet + + + + + + order book (≈180 KB) + + asks_root: Alice's ask, 10 @ 265 + bids_root: Bob's bid, 1 @ 250 + next_order_id: 4 @@ -223,19 +220,8 @@ is_active: true - - A CLIENT KEYPAIR - - - order book (≈180 KB) - - asks_root: Alice's ask, 10 @ 265 - bids_root: Bob's bid, 1 @ 250 - next_order_id: 4 - - - A CLIENT KEYPAIR + "BASE_VAULT" + MARKET ADDRESS base vault @@ -245,7 +231,7 @@ - A CLIENT KEYPAIR + "QUOTE_VAULT" + MARKET ADDRESS quote vault @@ -255,7 +241,7 @@ - A CLIENT KEYPAIR + "FEE_VAULT" + MARKET ADDRESS fee vault @@ -275,4 +261,5 @@ each side calls settle_funds() + not drawn: the NVDAx and USDC mints, which these accounts name only as seeds and fields diff --git a/building-financial-products-in-solana/figures/order-book-settle.svg b/building-financial-products-in-solana/figures/order-book-settle.svg index 0aadc0b6..c1529f8b 100644 --- a/building-financial-products-in-solana/figures/order-book-settle.svg +++ b/building-financial-products-in-solana/figures/order-book-settle.svg @@ -1,23 +1,17 @@ - + Settlement pays Bob and Carol - Solana Devnet - + Solana Devnet + + - - - - NVDAx mint - - - - - - - USDC mint + + + + order book (≈180 KB) @@ -212,16 +206,9 @@ quote_lot_size: 1 is_active: true - - - A CLIENT KEYPAIR - - - order book (≈180 KB) - - A CLIENT KEYPAIR + "BASE_VAULT" + MARKET ADDRESS base vault @@ -231,7 +218,7 @@ - A CLIENT KEYPAIR + "QUOTE_VAULT" + MARKET ADDRESS quote vault @@ -241,7 +228,7 @@ - A CLIENT KEYPAIR + "FEE_VAULT" + MARKET ADDRESS fee vault @@ -267,4 +254,5 @@ Pay Bob 250 USDC from the quote vault 3 Pay Carol 748.125 USDC in quote + not drawn: the NVDAx and USDC mints, which these accounts name only as seeds and fields diff --git a/building-financial-products-in-solana/figures/order-book-withdraw-fees.svg b/building-financial-products-in-solana/figures/order-book-withdraw-fees.svg index 869b944c..8c5f389a 100644 --- a/building-financial-products-in-solana/figures/order-book-withdraw-fees.svg +++ b/building-financial-products-in-solana/figures/order-book-withdraw-fees.svg @@ -1,23 +1,17 @@ - + Maria withdraws the fees - Solana Devnet - + Solana Devnet + + - - - - NVDAx mint - - - - - - - USDC mint + + + + order book (≈180 KB) @@ -200,29 +194,22 @@ is_active: true - - A CLIENT KEYPAIR - - - order book (≈180 KB) - - - A CLIENT KEYPAIR + "BASE_VAULT" + MARKET ADDRESS base vault - A CLIENT KEYPAIR + "QUOTE_VAULT" + MARKET ADDRESS quote vault - A CLIENT KEYPAIR + "FEE_VAULT" + MARKET ADDRESS fee vault @@ -238,4 +225,5 @@ an empty fee vault succeeds as a no-op, so Maria can run withdraw_fees() on a schedule without babysitting it + not drawn: the NVDAx and USDC mints, which these accounts name only as seeds and fields diff --git a/building-financial-products-in-solana/order-book.md b/building-financial-products-in-solana/order-book.md index 80a1d5dc..6fa3cfa5 100644 --- a/building-financial-products-in-solana/order-book.md +++ b/building-financial-products-in-solana/order-book.md @@ -40,9 +40,9 @@ The **market** is a PDA holding one trading pair's configuration: the two mint a The **order book** is the largest account: about 180 KB holding two trees of resting orders (one per side, 1,024 orders each) plus a monotonic `next_order_id` counter. It is **zero-copy**: an account this large would be prohibitively expensive to deserialize on every instruction, so the program reads and writes fields in place through an `AccountLoader` instead. -> **Why the order book is not a PDA.** Solana caps account allocations made from inside a program at 10 KB, so `initialize_market()` cannot create a 180 KB account. Instead, the client creates the account directly with the System Program before calling `initialize_market()`, sized to `ORDER_BOOK_ACCOUNT_SIZE` and owned by the program. And it cannot be a PDA at all, because creating an account that way requires the new account's own signature, which a PDA cannot give. The program instead ties the book to its market with a stored-address check: the market records the book's address, and every instruction that touches the book checks it against that record. +> **Why the order book is not a PDA.** Solana caps account allocations made from inside a program at 10 KB, so `initialize_market()` cannot create a 180 KB account. Instead, the client creates the account directly with the System Program before calling `initialize_market()`, sized to `ORDER_BOOK_ACCOUNT_SIZE` and owned by the program. And it cannot be a PDA at all, because creating an account that way requires the new account's own signature, which a PDA cannot give. The program instead ties the book to its market with a stored-address check: the market records the book's address, and every instruction that touches the book checks it against that record. So the book's address is a public key, and the figures draw its dot on the curve, with the book's box above it at the top right. Its private key signs once, at creation; after that the program owns the account and the key can do nothing to it. -Three **vaults** hold the money: a base vault for locked NVDAx, a quote vault for locked USDC, and a separate **fee vault** for Maria's earnings. The fee vault holds the same token as the quote vault, and keeping them separate means balances owed to users and balances earned by the market can never be confused. That separation is what stops a caller from being paid out of Maria's fees; the failure modes section shows the attack it blocks. +Three **vaults** hold the money: a base vault for locked NVDAx, a quote vault for locked USDC, and a separate **fee vault** for Maria's earnings. Unlike the book, all three are PDAs of the market (seeds: `"base_vault"`, `"quote_vault"` or `"fee_vault"`, then the market), created by `initialize_market()` with the market as their authority. The fee vault holds the same token as the quote vault, and keeping them separate means balances owed to users and balances earned by the market can never be confused. That separation is what stops a caller from being paid out of Maria's fees; the failure modes section shows the attack it blocks. ## Every Trader Gets a Ledger Line diff --git a/scripts/check-figure-geometry.mjs b/scripts/check-figure-geometry.mjs index 385295fe..cc8b52de 100644 --- a/scripts/check-figure-geometry.mjs +++ b/scripts/check-figure-geometry.mjs @@ -10,6 +10,8 @@ // 6. The icon and title centre on their band: the box, or the title bar. // 7. A faded address dot still occludes the cluster curve. // 8. Every label sits inside the canvas. +// 9. A public-key dot (r=6) sits on the curve; a derived-address dot (r=5.5) sits clear of it. +// 12. Every address dot sits beside an account box, legend keys aside. // 11. No address dot touches or overlaps an account box. // // Usage: node scripts/check-figure-geometry.mjs [name-fragment ...] @@ -322,10 +324,45 @@ for (const file of selected) { } } + // Every address dot in root coordinates, with its distance to the nearest + // piece of curve and to the nearest account box. Both are measured after + // transforms: the lending and vault figures place whole columns with a + // translate, and a dot's own cx/cy says nothing about where it prints. + // The legend arcs count as curve: a dot on one of them is drawn as on the + // curve. + const toRoot = (el) => inv.multiply(el.getScreenCTM()); + const curvePoints = [...svg.querySelectorAll("path")] + .filter((p) => p.getAttribute("stroke") === "#111" && p.getAttribute("stroke-width") === "3.5") + .flatMap((piece) => { + const m = toRoot(piece), len = piece.getTotalLength(), points = []; + for (let i = 0; i <= 400; i += 1) points.push(piece.getPointAtLength((len * i) / 400).matrixTransform(m)); + return points; + }); + const accountRects = [...svg.querySelectorAll('rect[rx="6"]')].map(box); + const italics = [...svg.querySelectorAll("text")] + .filter((t) => t.getAttribute("font-style") === "italic").map(box); + const addressDots = nodes.filter(isDot).map((c) => { + const b = box(c); + const cx = (b.x1 + b.x2) / 2, cy = (b.y1 + b.y2) / 2; + const r = parseFloat(c.getAttribute("r")); + let near = Infinity; + for (const p of curvePoints) near = Math.min(near, Math.hypot(p.x - cx, p.y - cy)); + // The ring's outer edge to the box's border, both ink. + let toBox = Infinity; + for (const q of accountRects) { + const dx = Math.max(q.x1 - cx, 0, cx - q.x2), dy = Math.max(q.y1 - cy, 0, cy - q.y2); + toBox = Math.min(toBox, Math.hypot(dx, dy) - r - 1); + } + // A legend key is a lone dot with its explanation in italic beside it. + const legend = italics.some((t) => t.x1 >= cx && t.x1 - cx <= 40 && Math.abs((t.y1 + t.y2) / 2 - cy) <= 12); + return { cx, cy, r, near, toBox, legend }; + }); + return { arrows, accounts, fadedDots, + addressDots, // Every address dot, with its 2-wide ring: getBBox stops at the // geometry, so the ring reaches a unit past it on every side. dots: nodes.filter(isDot).map((c) => { const b = box(c); return { x1: b.x1 - 1, y1: b.y1 - 1, x2: b.x2 + 1, y2: b.y2 + 1 }; }), @@ -557,6 +594,41 @@ for (const file of selected) { `draw an opaque white disc behind it, before its faded group`); } + // 9. The dot's radius and its place against the curve say the same thing, + // and have to agree. An address made from a key pair is a public key, so it + // is on the curve - always, whoever generated the pair and whatever they did + // with the private key afterwards - and draws as an r=6 dot touching the + // curve. A PDA or a built-in address has no private key and never sits on + // it: r=5.5, clear of the curve, beside its seeds or its address. The + // order-book figures once drew four accounts created from client key pairs + // as r=5.5 dots hanging in the program column, captioned "A CLIENT KEYPAIR", + // which said "no private key" and "a private key" about the same address. + for (const d of fig.addressDots) { + if (!Number.isFinite(d.near)) continue; // a figure with no curve at all + const where = `${d.cx.toFixed(0)},${d.cy.toFixed(0)}`; + if (d.r === 6 && d.near > d.r + 1.75) { + fail(`public-key dot at ${where} sits ${(d.near - d.r - 1.75).toFixed(1)} off the curve; ` + + `an address made from a key pair is always on the curve: move the dot onto it, with its box beside it, ` + + `or if the address is derived draw it r=5.5 beside its seeds`); + } else if (d.r === 5.5 && d.near < d.r + 1.75 + 4) { + fail(`derived-address dot at ${where} touches the curve; a PDA or built-in address is never on the curve: ` + + `move it clear, or if the address is a public key draw it r=6 on the curve`); + } + } + + // 12. An address and its account are one thing drawn twice, so the dot + // sits beside its box: at most 20 from ring to border, the ceiling + // DIAGRAMS.md sets. A dot that cannot reach its box is a layout to change, + // not a label to add: the first fix for the order book's vaults named four + // dots on the curve and left their boxes 300 units below them. Legend keys + // are the one lone dot allowed, and they carry their explanation in italic + // beside them. + for (const d of fig.addressDots) { + if (d.legend || d.toBox <= 20) continue; + fail(`address dot at ${d.cx.toFixed(0)},${d.cy.toFixed(0)} is ${Number.isFinite(d.toBox) ? d.toBox.toFixed(0) : "far"} from the nearest account box; ` + + `a dot sits within 20 of its account's box: move the box to the dot, or the dot to the box`); + } + // 8. The PDF sets the figure at the text width and clips it to the viewBox, // so a label past the canvas edge is not a label that runs into the margin: // it is a label with its end cut off. Anything past the edge fails; a label diff --git a/scripts/lint-manuscript.mjs b/scripts/lint-manuscript.mjs index 738d25df..721235d2 100644 --- a/scripts/lint-manuscript.mjs +++ b/scripts/lint-manuscript.mjs @@ -47,6 +47,13 @@ const rules = [ // stands in for a mechanism rather than stating it, so the fix is the same // fix every time: name the thing and say what it does. [/\bload.bearing\b/i, 'say what depends on the thing and what breaks without it'], + // A key pair is two keys. Its public key is an address; the pair is not an + // address or an account, the private key is never shown, and an address + // made from a key pair is always on the curve - only a PDA is off it. The + // order-book figures once captioned four such addresses "A CLIENT KEYPAIR" + // beside dots drawn off the curve, which is every one of these at once. + [/\bkey ?pairs?(?:'s)? (?:address|account|PDA)\b|\b(?:address|account|PDA) of (?:a|an|the|its|their|each) (?:\w+ )?key ?pairs?\b|\b(?:off|on)[- ]curve key ?pairs?\b|\bkey ?pairs? (?:that (?:is|are|sits?) )?(?:off|on) the curve|\bPDA key ?pairs?\b|\bkey ?pair PDAs?\b|\bderived key ?pairs?\b/i, + 'a key pair is two keys, of which the public one is the address: the pair is neither an address nor an account, and an address made from one is always on the curve (companion AI plugin)'], [/\b(delve|tapestry|intricate|multifaceted|holistic|myriad|seamlessly|seamless|robust|pivotal|crucial|elevate|testament to|deep dive|paradigm shift|game.chang(er|ing)|unlock the power|realm of|landscape of|in today's)\b/i, "reads as machine-written; name the specific thing instead"], [/it(?:'s| is) worth noting/i, "cut it: if the point is worth making, make it"], @@ -530,6 +537,15 @@ const svgRules = [ // does not belong in the label. [/>ATA PROGRAM/, 'seed lists name seeds, not the deriving program: an ATA is OWNER\'S ADDRESS + TOKEN PROGRAM + MINT'], + // No caption names a key pair. An address made from one is a public key, + // so it is an unlabeled dot on the curve, with its account's box beside + // it. A caption reading "A CLIENT KEYPAIR" + // beside a dot drawn off the curve called the same address a PDA and a + // public key at once, and called the key pair an address, which it is not. + // The legend lines that teach the curve say "key pair" in lower case at + // body size; a caption is 7 units, and the caps form is a caption's. + [/]*font-size="7"[^>]*>[^<]*\b[Kk][Ee][Yy] ?[Pp][Aa][Ii][Rr]|]*>[^<]*\bKEY ?PAIRS?\b/, + 'no caption names a key pair: an address made from one is an unlabeled r=6 dot on the curve beside its account box, never a labeled offcurve dot (DIAGRAMS.md)'], // The coin mark is three rings. Two rings read as a pair of address dots at // print size, where the accent colour is gone and the mark is about 2mm. [/]*stroke="#1e7a3c"/, diff --git a/solana-building-blocks/figures/token-accounts-and-atas.svg b/solana-building-blocks/figures/token-accounts-and-atas.svg index 6a6981c5..f9467c97 100644 --- a/solana-building-blocks/figures/token-accounts-and-atas.svg +++ b/solana-building-blocks/figures/token-accounts-and-atas.svg @@ -28,9 +28,9 @@ anyone can compute where it is any other token account - at whatever address it was created + at whatever address its program picks - A CLIENT KEYPAIR + "BASE_VAULT" + MARKET ADDRESS @@ -38,5 +38,5 @@ authority: the market PDA - the program stores its address + here, a PDA of the market diff --git a/solana-building-blocks/figures/token-accounts-off-the-curve.svg b/solana-building-blocks/figures/token-accounts-off-the-curve.svg index 56e22b7d..ae6d28ef 100644 --- a/solana-building-blocks/figures/token-accounts-off-the-curve.svg +++ b/solana-building-blocks/figures/token-accounts-off-the-curve.svg @@ -1,5 +1,5 @@ - Token accounts are always off curve + Derived token accounts sit off the curve Solana Devnet @@ -59,7 +59,7 @@ 500 USDC - + on the curve: a public key, and someone holds the private key that signs for it