From bf161813a2c674734ff93fda4122ddd3dc9e227e Mon Sep 17 00:00:00 2001 From: shum Date: Wed, 14 Oct 2026 10:32:03 +0000 Subject: [PATCH] plans: simpler names sequence diagrams --- .../names-codes/2026-10-13-names-sequence.md | 1197 ++++++++++++----- 1 file changed, 874 insertions(+), 323 deletions(-) diff --git a/plans/names-codes/2026-10-13-names-sequence.md b/plans/names-codes/2026-10-13-names-sequence.md index d5db16c12e..31ebd928cc 100644 --- a/plans/names-codes/2026-10-13-names-sequence.md +++ b/plans/names-codes/2026-10-13-names-sequence.md @@ -1,29 +1,97 @@ # SimpleX names store: the flows, step by step -Every flow of the names store as a sequence of calls, to check each step and find what is missing. The spec is [`2026-10-09-name-codes.md`](2026-10-09-name-codes.md), the plan [`2026-10-09-names-web-store-plan.md`](2026-10-09-names-web-store-plan.md). +This document shows every flow of the names store as a sequence of calls. Its purpose is to check each step and find what is missing. + +- The spec is [`2026-10-09-name-codes.md`](2026-10-09-name-codes.md), and the plan is [`2026-10-09-names-web-store-plan.md`](2026-10-09-names-web-store-plan.md). +- The words used here (owner code, commitment, intent, sponsor, the lag and others) are defined in the spec, under [Words used here](2026-10-09-name-codes.md#words-used-here). + +## The whole flow at a glance + +Three parts talk to each other: +- the **webapp** in the browser; +- the **service** (StoreService); +- the **chain host**, where the resolver reads names from the Ethereum node (reth, with nimbus as its consensus client). + +The payment provider only takes the money. Sections 1–13 below show every step in detail. This diagram is the happy path for one paid name. + +```mermaid +sequenceDiagram + autonumber + participant W as Webapp + participant S as Service + participant P as BTCPay or Stripe + box Chain host + participant R as Resolver + participant E as Ethereum node
reth + nimbus + end + + Note over W,E: Search + W->>S: GET /api/name/:hash + S->>R: GET /v2/resolve/[hash].simplex + R->>E: eth_call (registry, resolver) + R-->>S: available + S-->>W: available, price + + Note over W,E: Order + W->>W: owner code → wallet, makeCommitment + W->>S: POST /api/invoice (hash, owner, commitment) + S->>P: create invoice + S-->>W: invoice to pay + + Note over W,E: Payment + W->>P: buyer pays + P->>S: webhook, then the poller reads it + S->>S: settle: registration queued + + Note over W,E: Commit (hides the name) + S->>E: SimplexController.commit(commitment) + E-->>S: receipt + S-->>W: phase committed (long poll) + + Note over W,E: Reveal, after 60 seconds + W->>W: sign RegisterName with the owner's key + W->>S: POST /api/registration/:id/reveal (name, secret, signature) + S->>S: verifyRegisterIntent + + Note over W,E: Register + S->>E: SimplexSponsor.registerWithCredit(registration, signature) + E-->>S: receipt: the name is the owner's + S-->>W: phase registered, show the owner code + + Note over W,E: Later: change the name's links + W->>W: sign SetText with the owner's key + W->>S: POST /api/name/records + S->>E: SimplexResolver.setTextWithSig +``` + +- The service pays the gas for every transaction from its own key, the registrar key. +- The sponsor's allowance pays the name's price. So the buyer's wallet never needs ether. +- The owner code is the only key to the name. +- Moving the name into the SimpleX app later uses `approveWithSig` and `claimWithSig` (section 12). Function names: - **†** exists on `origin/ab/names-registration-primitives-sponsored` (Alain's primitives) or its simplexmq branch; - **‡** exists in this repo today; - unmarked names are proposed here. -Participants: -- **Page**: the names webapp in the browser (`wallet.ts`, `names.ts`, the library's `api.ts` and `store.ts`). -- **Service**: StoreService's web API. -- **Worker**: StoreService's registration worker and its one transaction sender. -- **DB**: StoreService's database. -- **Resolver**: `snrc-resolve.py`, by hash. -- **Pay**: BTCPay or Stripe. -- **Chain**: the reth node and the PR #35 contracts (`SimplexController`, `SimplexSponsor`, `SimplexResolver`). -- **App**: the SimpleX app. +Participants, the same in every diagram: +- **Webapp**: the names webapp in the browser (`wallet.ts`, `names.ts`, the library's `api.ts` and `store.ts`). +- **Service**: StoreService. This is its web API, its registration worker with the one transaction sender, and its database. +- **BTCPay or Stripe**: the payment provider. +- **Resolver**: `snrc-resolve.py` on the chain host. It reads names by hash from the Ethereum node. +- **Ethereum node**: reth (with nimbus) and the PR #35 contracts (`SimplexController`, `SimplexSponsor`, `SimplexResolver`, the base registrar). +- **SimpleX app**: the app. It redeems names, and it opens orders for its own wallet. + +Each section shows the calls first. The bullets under it hold the exact rules. ## Contents +0. The whole flow at a glance (above) 1. Search 2. Checkout, owner code and order 3. Payment and settlement 4. Commit -5. Reveal and the owner's intent +5. Reveal: the owner signs the registration 6. Register and confirm 7. A prepaid code 8. Taken: another name, or a prepaid code @@ -36,412 +104,895 @@ Participants: ## 1. Search +The buyer types a name, and the page sends only its hash and length. The service answers whether the name is available, and at what price. + ```mermaid sequenceDiagram - participant Page - participant Service - participant DB - participant Resolver - participant Chain - Page->>Page: fold to lower case, validateLabel(label) - Page->>Page: labelHash = keccak256(label) - Page->>Service: GET /api/name/:labelHash?length=n - Service->>Service: rate limit (search, its own), length 6–63 else invalid_name - Service->>Resolver: GET /v2/resolve/[labelHash as 64 lowercase hex, no 0x].tld - Service->>DB: registrationByHash(labelHash): registering, or registered within the last hour? - alt the resolver says available, and our registration is registering, or registered within the last hour - Service-->>Page: registered{entities: [], tld} (no owner until the resolver has it) - else - alt available - Service->>DB: namePrice(labelHash, n) (Catalog.namePrice) - Service-->>Page: available{price, currency, years, resolver, sponsor, chainId, tld} - else registered - loop each simplexContact / simplexChannel link - Service->>Service: linkProfile(link) (APIConnectPlan ‡, its own rate limit, cached, 3 s) - end - Note over Service: owner and resolver from the resolver's answer for the hashed node - Service->>Chain: when the answer's resolver is the default one: SimplexResolver.nonces(owner), text(node, simplex.contact), text(node, simplex.channel), node = keccak256(namehash(tld) ‖ labelHash), else records and recordsNonce null - Note over Service: a failed chain read → provider_unavailable - Service-->>Page: registered{entities, records, owner, resolver, recordsNonce, chainId, tld} - else 502 labelNotRecorded (impossible on PR #35, where every registration records its label), or upstreamError, tldNotConfigured, noPriceOracle, unknown, or no answer - Service-->>Page: provider_unavailable - else reserved - Service-->>Page: reserved - end + autonumber + participant W as Webapp + participant S as Service + participant R as Resolver + participant E as Ethereum node + W->>W: lower-case, validLabel, keccak256 → labelHash + W->>S: GET /api/name/:labelHash?length=n + S->>R: GET /v2/resolve/[labelHash].simplex + R->>E: eth_call: registry, registrar, controller + R-->>S: available, registered or reserved + alt available + S->>S: namePrice(labelHash, n) + S-->>W: available (price, sponsor, chainId) + else registered + S->>S: linkProfile(link) for each link + S->>E: nonces(owner), text(node, contact), text(node, channel) + S-->>W: registered (cards, owner, records) + else reserved + S-->>W: reserved end ``` +- **The page's check:** the page folds the name to lower case first. Then `validateLabel` in `names.ts` checks it, mirroring `validLabel`. +- **Checks first:** the service checks the search rate limit, then the length. The length must be 6–63, else the answer is `invalid_name`. +- **The price:** `namePrice` is `Catalog.namePrice`. +- **Hash formats:** + - `:labelHash` is `0x` + 64 lowercase hex. + - The resolver path takes 64 lowercase hex without `0x`. +- **The lag:** the resolver may say available while our own registration (`registrationByHash(labelHash)`) is `registering`, or `registered` within the last hour. + - Then the answer is `registered{entities: [], tld}`, with no owner. + - The page then takes the nonce, chain id and resolver from the registration view. +- **The answers:** + - `available{price, currency, years, resolver, sponsor, chainId, tld}`; + - `registered{entities, records, owner, resolver, recordsNonce, chainId, tld}`. +- **Owner and records:** + - `owner` and `resolver` come from the resolver's answer. + - `records` and `recordsNonce` are read from the chain, on the default resolver, with `node = keccak256(namehash(tld) ‖ labelHash)`. + - They are read only when the answer names the default resolver. Otherwise they are null. +- **Link cards:** for each `simplexContact` and `simplexChannel` link, `linkProfile` uses `APIConnectPlan` ‡. It has its own rate limit, a cache and a 3 s timeout. +- **Failures:** any resolver error answers `provider_unavailable`. A failed chain read answers the same. The resolver errors are: + - `upstreamError`, `tldNotConfigured`, `noPriceOracle`, `unknown`; + - no answer; + - `labelNotRecorded`, which cannot happen on PR #35. + ## 2. Checkout, owner code and order +The page makes the owner code, the owner wallet and the commitment in the browser, then places the order. The order carries the name's hash, the commitment and the sealed copy, not the name itself. + ```mermaid sequenceDiagram - participant Page - participant Service - participant DB - participant Pay - Page->>Page: an open order with a submitted card payment? then Continue is off (N3e), and nothing below runs - Page->>Page: newOwnerCode() (as randomNameCode †): entropy (128 bits), code = SN- + entropy + SHA-256[22 bits] - Page->>Page: walletFromCode(code) (as the test mock †): BIP-39 seed, m/44'/60'/0'/0/0 key, address - Page->>Page: secret = HMAC-SHA256(seed, "simplex.domains commitment " + label) - opt links chosen (N3g) - Page->>Page: each key's links joined with #59;, each value within 4096 bytes - Page->>Service: POST /api/link/preview {link} (section 11) + autonumber + participant W as Webapp + participant S as Service + W->>W: newOwnerCode() → owner code + W->>W: walletFromCode(code) → key, owner address + W->>W: secret = HMAC(seed, name) + opt links chosen + W->>S: POST /api/link/preview (section 11) + S-->>W: link card end - Page->>Page: registration = {label, owner, duration (2 years × 365 days), secret, resolver, data = recordCalls(registration) (as †, internal to Registration.hs)} - Page->>Page: commitment = makeCommitment(registration) (as makeCommitment †) - Page->>Page: sealed = AES-256-GCM(HMAC-SHA256(seed, "simplex.domains sealed"), {label, contact, channel}) - Page->>Page: saveOrder(code, label, links, registration) (store.ts) - Page->>Page: N3h: show the code, wait for "I have saved my code" - Page->>Service: POST /api/invoice {kind: name, labelHash, length, price, method, owner, commitment, sealed} - Service->>Service: rate limit (create), validate owner, length 6–63 else invalid_name, sealed within its bound else bad_request - Service->>DB: this commitment: an open invoice → the same order answered with it, a different one bad_request with that invoiceId (the page opens it), a registration past unpaid → answered with its order (invoiceId null and the registration for a code one) - Service->>Service: resolver by hash: registered or reserved → name_taken, a resolver failure → name_check_unavailable, nothing created - Service->>DB: resolver says available but our registration is registering, or registered within the last hour → name_taken - Service->>DB: namePrice(labelHash, length) = price? else catalog_changed{price} - Service->>Pay: create invoice (BTCPay / Stripe ‡) - Service->>DB: insert name_registrations (phase unpaid), name_invoices, invoices ‡ - Service-->>Page: {invoiceId, the badge invoice's common fields, kind: name, registration{id, phase: unpaid}} - Page->>Page: saveOrder(orderId = invoiceId, registrationId), or with invoiceId null (a code registration on this commitment) open ?registration= + W->>W: commitment = makeCommitment(registration) + W->>W: sealed = encrypt(name, links) with the seed + W->>W: saveOrder(code, name, links) + W->>W: show the owner code, wait for "I have saved my code" ``` +Then the order itself: + +```mermaid +sequenceDiagram + autonumber + participant W as Webapp + participant S as Service + participant R as Resolver + participant P as BTCPay or Stripe + W->>S: POST /api/invoice (labelHash, length, price, method, owner, commitment, sealed) + S->>S: validate, check this commitment's other orders + S->>R: is the name still free? + R-->>S: available + S->>S: namePrice = price? + S->>P: create invoice + P-->>S: invoice + S->>S: save registration (unpaid) and invoice + S-->>W: invoice and registration (unpaid) + W->>W: saveOrder(orderId = invoiceId, registrationId) +``` + +- **Before checkout:** if this browser has an open order whose card payment awaits confirmation, Continue is off (N3e). +- **Page-side functions:** + - `newOwnerCode` works as `randomNameCode` †. It takes 128 bits of entropy. The code is `SN-` + the entropy + 22 bits of SHA-256. + - `walletFromCode` works as the test mock †. It makes a BIP-39 seed, the key at `m/44'/60'/0'/0/0`, and its address. +- **Registration fields:** + - `secret = HMAC-SHA256(seed, "simplex.domains commitment " + label)`. + - The links of one key are joined with ";". Each value is within 4096 bytes. + - `registration = {label, owner, duration (2 years × 365 days), secret, resolver, data = recordCalls(registration) †}`. + - `makeCommitment` is †. +- **Sealing:** `sealed = AES-256-GCM(HMAC-SHA256(seed, "simplex.domains sealed"), {label, contact, channel})`. +- **The service's checks, in order:** + 1. the create rate limit; + 2. the owner address; + 3. the length (`invalid_name`); + 4. `sealed` within its bound (`bad_request`); + 5. this commitment's other orders (below); + 6. the resolver: registered or reserved is `name_taken`. A resolver failure is `name_check_unavailable`, and nothing is created; + 7. the lag: our own registration `registering`, or `registered` within an hour, is `name_taken`; + 8. `namePrice` must equal the price, else `catalog_changed{price}`. +- **This commitment's other orders:** + - An open invoice for the same order is answered with that invoice. + - A different order gets `400 {"error": "bad_request", "invoiceId"}`, and the page opens that order. + - A registration past `unpaid` is answered with its order. For a code registration, `invoiceId` is null, and the page opens `?registration=`. +- **Storage:** the service inserts `name_registrations` (phase `unpaid`), `name_invoices` and `invoices` ‡. + ## 3. Payment and settlement +The buyer pays the provider. The service then reads the invoice itself and settles it, which queues the registration. + ```mermaid sequenceDiagram - participant Page - participant Service - participant DB - participant Pay - Page->>Service: GET /api/invoice/:id?wait&seenPhase (long poll ‡, the phase added) - Pay-->>Service: POST badges.simplex.chat/webhooks/{btcpay,stripe} ‡ - Service->>DB: getInvoiceByProviderRef ‡, extended to name_invoices, as are the poller's scans, its expiry update and GET /api/invoice/:id - Service->>Service: queueReadHint(ref) ‡: the webhook only hints - Service->>Pay: the poller ‡ reads the invoice status, now or on its next pass - Service->>DB: settle in one transaction: invoice paid, and if it was still open, registration unpaid → queued (both published after the commit), else the registration stays unpaid and the operator is alerted to refund - Service-->>Page: invoice settled, registration{phase: queued} - Note over Service: the poller carries the authority, as for badges today - opt Get a new invoice, the old one maybe open (card form not loading, payment details unavailable) - Page->>Service: POST /api/invoice/:id/cancel (create limit) - alt cancelled (200 expired, nothing received) - Page->>Service: POST /api/invoice, the same commitment (section 2) - else 409 funded or 503 - Page->>Page: keep the old order - else 200 still open - Page->>Page: show CANCEL_STILL_OPEN, keep the old order - else 409 not_open, 200 paid, or 200 expired with payment marks - Page->>Service: GET /api/invoice/:id: paid with the registration past unpaid → show it, paid with phase unpaid → order again, else holding a payment → keep it, expired with nothing received → order again - end + autonumber + participant W as Webapp + participant S as Service + participant P as BTCPay or Stripe + W->>S: GET /api/invoice/:id (long poll) + W->>P: buyer pays + P->>S: webhook (only a hint) + S->>P: poller reads the invoice + P-->>S: paid + S->>S: settle: invoice paid, registration queued + S-->>W: paid, phase queued +``` + +- **Webhooks:** they arrive at `POST badges.simplex.chat/webhooks/{btcpay,stripe}` ‡. + - A webhook only triggers a read (`queueReadHint` ‡). + - The poller carries the authority, as for badges today. +- **Name invoices in today's code:** `getInvoiceByProviderRef` ‡, the poller's scans, its expiry update and `GET /api/invoice/:id` all gain a name side. +- **Settlement** is one database transaction: + - the invoice becomes paid; + - the registration goes `unpaid` → `queued`. + - Both changes are published to the long poll after the commit. +- **Late payment:** an invoice paid after it expired or was canceled leaves the registration `unpaid`. The operator is alerted to refund it (gap 26). +- **The long poll** carries `wait` and `seenPhase`, so it also wakes on phase changes. + +**Get a new invoice.** The old invoice may still be open when the card form does not load, or the payment details are unavailable. The page then cancels it before ordering again: + +```mermaid +sequenceDiagram + autonumber + participant W as Webapp + participant S as Service + W->>S: POST /api/invoice/:id/cancel + alt cancelled, nothing received + W->>S: POST /api/invoice, same commitment (section 2) + else 409 funded, 503, or still open + W->>W: keep the old order + else 409 not_open, paid, or holding a payment + W->>S: GET /api/invoice/:id + S-->>W: the order as it is now + W->>W: show it, keep it, or order again end ``` +- **The cancel** uses the create rate limit. +- **A `200` that is still `open`:** the page shows `CANCEL_STILL_OPEN`. +- **After the re-read:** + - paid, with the registration past `unpaid`: show the order; + - paid, with the registration still `unpaid` (paid after it closed): order again; + - otherwise holding a payment: keep the order; + - expired with nothing received: order again. + ## 4. Commit +The service puts the commitment on chain. This reserves the name without showing it, so nobody can see the name and register it first. + ```mermaid sequenceDiagram - participant Page - participant Service - participant Worker - participant DB - participant Chain - Worker->>DB: next queued registration - Worker->>Worker: enqueue on the registrar's one sender - Worker->>Chain: sendCommit(commitment) †, split to sign first: eth_estimateGas, nonce, fees, sign, hash = keccak256(raw transaction) - Worker->>DB: phase committing, commit_tx (that hash and its raw transaction, saved before sending) - Worker->>Chain: eth_sendRawTransaction + autonumber + participant W as Webapp + participant S as Service + participant E as Ethereum node + S->>S: next queued registration + S->>E: eth_estimateGas: SimplexController.commit(commitment) + S->>S: sign, save the tx hash and raw tx + S->>E: eth_sendRawTransaction loop until mined - Worker->>Chain: eth_getTransactionReceipt(commitTx) + S->>E: eth_getTransactionReceipt end - Note over Worker: a commit reverting, in its dry run (nothing sent) or mined with status 0, with commitments() + maxCommitmentAge still ahead is UnexpiredCommitmentExists, commit's only revert: the commitment is live, so the registration becomes committed with the reveal times from commitments(). A commit reverting with no live commitment (out of gas, or a commitment that lapsed by the read) is sent (again, if it was mined), a second later when the read falls at commitments(commitment) + maxCommitmentAge itself, as the retry path waits at revealBefore. When the commitment had lapsed, label, secret, records, intent_*, error, the tx hashes and blocks, reveal_after and reveal_before are also cleared before the new commit is signed, so the page signs again - Worker->>Chain: eth_call SimplexController.minCommitmentAge(), maxCommitmentAge() - Worker->>DB: phase committed, reveal_after = commit block time + minCommitmentAge, reveal_before = commit block time + maxCommitmentAge - Page->>Service: (long poll with seenPhase, read limit) registration{phase: committed, revealAfter, revealBefore} + S->>E: minCommitmentAge(), maxCommitmentAge() + S->>S: phase committed, revealAfter, revealBefore + S-->>W: phase committed (long poll) ``` -## 5. Reveal and the owner's intent +- **One sender:** the worker and the records relay share one sender for the registrar key. + - The sender assigns the transaction nonce. + - It saves each hash and raw transaction before sending. + - `sendCommit` † is split so it signs first. +- **Stored hash:** the commit's hash is `commit_tx`. The view shows it as `commitTx`. +- **The ages** are read with `SimplexController.minCommitmentAge()` and `maxCommitmentAge()`. +- **Reveal window:** + - `reveal_after` is the commit's block time + `minCommitmentAge`. + - `reveal_before` is the block time + `maxCommitmentAge`. +- **A commit that reverts**, in its dry run or mined with status 0: + - **The commitment is already live:** `commitments()` + `maxCommitmentAge` is still ahead. This is `UnexpiredCommitmentExists`. The registration becomes committed, with the times from `commitments()`. + - **No live commitment** (out of gas, or it lapsed by the read): the commit is sent again. When the read falls exactly at the lapse second, the worker waits a second first. + - **The commitment had lapsed:** before committing again, the service clears `label`, `secret`, `records`, `intent_*`, `error`, the tx hashes and blocks, and `reveal_after`/`reveal_before`. The page then signs again. + +## 5. Reveal: the owner signs the registration + +Once `revealAfter` has passed, the page reveals the name and signs the intent. The intent lets the service register the name for the owner. ```mermaid sequenceDiagram - participant Page - participant Service - participant DB - Page->>Page: wait until revealAfter - Page->>Page: nonce = registration view's intentNonce (SimplexSponsor.nonces(owner), 0 for a new code), deadline = revealBefore - Page->>Page: digest = RegisterName(label, owner, resolver, data, nonce, deadline) under sponsorDomain(chainId, sponsor) † - Page->>Page: signature = sign(digest, key) (@noble/secp256k1) - Page->>Service: POST /api/registration/:id/reveal {label, secret, records, nonce, deadline, signature} - Service->>Service: rate limit (read), a bad_request counts against it - Service->>DB: phase committed and label null? once label is set, an identical repeat → 200 in any phase, else bad_request - Service->>Service: validLabel(label) (#7390, ported and tightened to simplexmq's hyphen rule), keccak256(label) = labelHash, length - Service->>Service: the hash and length fix its price as the registration's price_id row, or the prepaid code covers the length - Service->>Service: links: SimpleX links of their kind, each record within 4096 bytes, resolver = default - Service->>Service: data = recordCalls(registration) † (internal), makeCommitment(registration) = stored commitment † - Service->>Service: verifyRegisterIntent(domain, now, intent) †: secret 32 bytes, deadline, signer = owner, then a low-s check of its own (the sponsor's SignatureChecker rejects high s) - Service->>Service: nonce = SimplexSponsor.nonces(owner), read from the chain (a failed read → provider_unavailable, not counted) - alt all hold - Service->>DB: label, secret, records, nonce, deadline, signature, ready to register - Service-->>Page: 200 + autonumber + participant W as Webapp + participant S as Service + participant E as Ethereum node + W->>W: wait until revealAfter + W->>W: sign RegisterName(label, owner, resolver, data, nonce, deadline) + W->>S: POST /api/registration/:id/reveal (label, secret, records, nonce, deadline, signature) + S->>S: rebuild the commitment, verifyRegisterIntent + S->>E: SimplexSponsor.nonces(owner) + alt all checks pass + S->>S: save the intent, ready to register + S-->>W: 200 else - Service-->>Page: bad_request (phase stays committed) + S-->>W: bad_request (stays committed) end ``` +- **The signature:** + - `nonce` is the view's `intentNonce` (0 for a new code). + - `deadline` is `revealBefore`. + - The digest uses `sponsorDomain(chainId, sponsor)` †. The page signs it with `@noble/secp256k1`. +- **The service's checks:** + - **Rate limit:** the read limit. Each `bad_request` counts against it. + - **Phase:** the registration must be `committed`, with `label` null. + - Once `label` is set, an identical repeat answers 200 in any phase. + - A different body is `bad_request`. + - **The name:** + - `validLabel` (#7390, ported and tightened to simplexmq's hyphen rule); + - `keccak256(label)` = `labelHash`, and the length matches; + - the price is the registration's `price_id` row, or the prepaid code covers the length. + - **Links and resolver:** + - each link is a SimpleX link of its kind; + - each record is within 4096 bytes; + - the resolver is the default one. + - **The commitment:** `data = recordCalls(registration)` †, and `makeCommitment(registration)` † must equal the stored commitment. + - **The intent:** + - `verifyRegisterIntent` † checks the 32-byte secret, the deadline, and that the signer is the owner; + - the service then checks for low s itself, because the sponsor's `SignatureChecker` rejects high s; + - the nonce must equal `SimplexSponsor.nonces(owner)`. + - **A failed chain read** answers `provider_unavailable`. It does not count as a `bad_request`. + ## 6. Register and confirm +The service sends the register, which gives the name to the owner. It shows the name as registered only after the receipt and 3 more blocks. + ```mermaid sequenceDiagram - participant Page - participant Service - participant Worker - participant DB - participant Chain - Worker->>DB: committed and revealed, now ≥ revealAfter - Worker->>Worker: verifyRegisterIntent † again: past the deadline the commitment has lapsed too, so phase failed and retry commits again, or reuses a live re-commit (section 9) - Worker->>Chain: sendRegister(intent) †: dry run of registerWithCredit(registration, nonce, deadline, signature) on SimplexSponsor - alt any revert while ownerOf(labelHash) = owner (resumed worker or duplicate send) - Worker->>DB: phase registered, registered_at, expires_at, spend the prepaid code - else dry run reverts NameNotAvailable or NameReserved, or any revert while available(label) is false - Worker->>DB: phase taken, release a prepaid code (section 8) - else dry run reverts while available(label) is true - Worker->>DB: retry, then phase failed (section 9) + autonumber + participant W as Webapp + participant S as Service + participant E as Ethereum node + S->>S: revealed and now ≥ revealAfter, verifyRegisterIntent again + S->>E: dry run: SimplexSponsor.registerWithCredit(registration, nonce, deadline, signature) + E-->>S: ok + S->>S: phase registering, save the tx hash and raw tx + S->>E: eth_sendRawTransaction + Note over E: sponsor checks the signature and nonce, spends the allowance, controller mints, sets the records, transfers the name to the owner + loop until the receipt + 3 blocks + S->>E: eth_getTransactionReceipt, eth_blockNumber + end + S->>E: ownerOf(tokenId) + S->>S: phase registered, spend the prepaid code if any + S-->>W: phase registered (long poll) +``` + +When the dry run or the mined transaction reverts: + +```mermaid +sequenceDiagram + autonumber + participant S as Service + participant E as Ethereum node + S->>E: ownerOf(labelHash) + alt the owner already holds it + S->>S: phase registered (a resumed or duplicate send) else - Worker->>DB: phase registering, register_tx (keccak256 of the signed transaction, with the raw transaction, saved before sending) - Worker->>Chain: eth_sendRawTransaction - Note over Chain: SimplexSponsor checks the signature and nonce, spends the registrar's allowance, calls SimplexController.registerSponsored, which mints to itself, applies the records by multicallWithNodeCheck, then transfers the name to the owner - loop until receipt + 3 blocks - Worker->>Chain: eth_getTransactionReceipt, eth_blockNumber + S->>E: available(label) + alt not available + S->>S: phase taken (section 8) + else available + S->>S: retry, then phase failed (section 9) end - Worker->>Chain: receipt status 1? ownerOf(tokenId) = owner? - Note over Worker: status 0 → ownerOf(labelHash) = owner is registered, else available(label) false is taken, true is failed - Worker->>DB: phase registered, registered_at, expires_at, spend the prepaid code, if any - Page->>Service: (long poll with seenPhase, read limit) registration{phase: registered} end ``` +- **Inside the register:** + 1. `SimplexSponsor` checks the signature and nonce, and spends the allowance. + 2. It then calls `SimplexController.registerSponsored`. + 3. The controller mints the name to itself, applies the records with `multicallWithNodeCheck`, and transfers the name to the owner. +- **The dry run** is the first step of `sendRegister` †. +- **Past the deadline:** `verifyRegisterIntent` fails, because the commitment has lapsed too. + - The registration goes to `failed`. + - Retry commits again, or reuses a live re-commit (section 9). +- **Reading reverts:** + - The dry run decodes `NameNotAvailable` or `NameReserved` as taken. + - Any other revert while `available(label)` is false is taken too. +- **Revert order:** the sponsor checks the intent and the allowance before the name's availability. So a taken name can revert with one of those first. +- **On taken:** a prepaid code is released. +- **On registered:** `registered_at` and `expires_at` are recorded. + ## 7. A prepaid code +The buyer pays with a prepaid code instead of money. The page checks the code first, then registers without an invoice. + ```mermaid sequenceDiagram - participant Page - participant Service - participant DB - Page->>Page: parseNameCode(text) → NCPrepaid (check character, else K6) (the sum type: proposed) - Note over Page: NCOwner here sends nothing and says: this is a name code, restore it in Your names - Page->>Service: POST /api/code/check {code} - Service->>DB: name_prepaid_codes by namePrepaidCodeHash - alt unknown code - Service-->>Page: 400 code_invalid - else - Service-->>Page: {minLength, years, state} - end - Page->>Page: apply: covered names show $0 and the code's years - Page->>Page: checkout as section 2, with duration = years × 365 days - Page->>Service: POST /api/name/register {labelHash, length, owner, commitment, sealed, code} - Service->>Service: rate limit (code check) - Service->>DB: a registration paying with this code, with this owner and commitment → that registrationId (a repeat), checked first - Service->>Service: validate owner, sealed within its bound else bad_request, length 6–63 else invalid_name, this commitment with an open invoice → bad_request with its invoiceId (the page cancels it first), with a registration past unpaid → its registrationId, with its invoiceId when paid (the page opens ?order=), then resolver by hash: registered or reserved → name_taken, a resolver failure → name_check_unavailable, nothing created - Service->>DB: resolver says available but our registration is registering, or registered within the last hour → name_taken - Service->>DB: code unused, length ≥ min_length, reserve it, registration queued - alt the code is not usable - Service-->>Page: code_invalid, code_used, code_reserved, code_expired, payment_pending or code_not_covering (K7, or K5 from the popup) - else - Service-->>Page: {registrationId}, plus invoiceId for a paid registration already on this commitment - end - Note over Page,DB: then sections 4 to 6, on ?registration= + autonumber + participant W as Webapp + participant S as Service + W->>W: parseNameCode(text) → prepaid code + W->>S: POST /api/code/check (code) + S-->>W: minLength, years, state + W->>W: covered names show $0 and the code's years + W->>W: checkout as section 2, duration = years × 365 days ``` +Then the registration: + +```mermaid +sequenceDiagram + autonumber + participant W as Webapp + participant S as Service + participant R as Resolver + W->>S: POST /api/name/register (labelHash, length, owner, commitment, sealed, code) + S->>S: a repeat of this code's registration? answer it + S->>S: validate, check this commitment's other orders + S->>R: is the name still free? + R-->>S: available + S->>S: code usable and covers the length? reserve it + S->>S: registration queued + S-->>W: registrationId + Note over W,S: then sections 4 to 6, on ?registration=id +``` + +- **Parsing:** `parseNameCode` returns `NCPrepaid` or `NCOwner`. The sum type is proposed. The page catches a typo in the check character (K6). +- **An owner code entered here** sends nothing. The page tells the buyer it is a name code, to restore under Your names. +- **The code check:** + - `POST /api/code/check` looks the code up in `name_prepaid_codes` by `namePrepaidCodeHash`. + - The code covers a name when its length ≥ `min_length`. + - An unknown code answers `400 code_invalid`. +- **Rate limits:** `/api/name/register` uses the code-check rate limit. +- **The service's checks, in order:** + 1. **The repeat check:** a registration paying with this code, with the same owner and commitment, answers its `registrationId`. This keeps a lost response safe. + 2. **Validation:** the owner, `sealed` within its bound (`bad_request`), and the length (`invalid_name`). + 3. **This commitment's other orders:** + - an open invoice answers `bad_request` with its `invoiceId`. The page cancels that invoice first. + - a registration past `unpaid` answers its `registrationId`, plus its `invoiceId` when it was paid. The page opens `?order=`. + 4. **The resolver:** registered or reserved is `name_taken`. A failure is `name_check_unavailable`. + 5. **The lag:** our own registration `registering`, or `registered` within the hour, is `name_taken`. +- **Code errors:** `code_invalid`, `code_used`, `code_reserved`, `code_expired`, `payment_pending` or `code_not_covering`. They are shown at checkout (K7), or in the popup (K5). + ## 8. Taken: another name, or a prepaid code +The name went to someone else before the register. A paid order can then switch to another name, or take a prepaid code instead. + ```mermaid sequenceDiagram - participant Page - participant Service - participant DB - Page->>Service: (long poll with seenPhase, read limit) registration{phase: taken} + autonumber + participant W as Webapp + participant S as Service + S-->>W: phase taken (long poll) alt another name (N8a) - Page->>Service: GET /api/name/:hash (section 1), price ≤ paid - Page->>Page: new secret, registration, commitment and sealed for the same wallet - Page->>Service: POST /api/registration/:id/name {labelHash, length, commitment, sealed, timestamp, signature by the owner key} - Service->>Service: rate limit (create), owner's signature and timestamp, paid registration in taken, sealed within its bound, invalid_name, name_taken as an order (a resolver failure → name_check_unavailable, nothing changed), namePrice ≤ paid, else bad_request - Service->>DB: price_id = that price row - Service->>DB: phase queued, new label hash, length, commitment and sealed, clear label, secret, records, intent_*, tx hashes and blocks, reveal_after, reveal_before, error - Note over Page,DB: then sections 4 to 6 again + W->>S: GET /api/name/:hash (section 1), price ≤ paid + W->>W: new commitment and sealed for the same wallet + W->>W: sign "simplex.domains name …" with the owner key + W->>S: POST /api/registration/:id/name (labelHash, length, commitment, sealed, timestamp, signature) + S->>S: checks, then queued with the new name + Note over W,S: then sections 4 to 6 again else a prepaid code (N8b) - Page->>Page: code = SN + payload from HMAC-SHA256(seed, "simplex.domains taken code " + registrationId), store it under Your names - Page->>Service: POST /api/registration/:id/code {codeHash, timestamp, signature by the owner key} - Service->>Service: rate limit (create), owner's signature and timestamp - Service->>DB: issued_code_id has this hash? then 200 (a repeat), checked right after the owner's signature - Service->>DB: paid registration in taken? else bad_request, insert the hash (current length tier, 2 years, paid, no expiry), issued_code_id, phase code_issued - Note over Page,DB: the same hash again for this registration answers the same, while a hash another code has is code_conflict, unreachable in practice for a derived code - Service-->>Page: 200 - else a prepaid-code order - Note over Page,DB: its code was released by the worker at taken, and it ends there + W->>W: derive the code from the seed and registrationId + W->>W: sign "simplex.domains code …" with the owner key + W->>S: POST /api/registration/:id/code (codeHash, timestamp, signature) + S->>S: save the code, phase code_issued + S-->>W: 200 end ``` +- **Another name (`/name`):** it uses the create rate limit. + - **Checks, in order:** + 1. the owner's signature and timestamp; + 2. a paid registration in `taken`; + 3. the order's checks: `sealed` within its bound, `invalid_name`, and `name_taken`. A resolver failure is `name_check_unavailable`, and nothing changes; + 4. `namePrice` must be ≤ the amount paid, else `bad_request`. + - **On success:** + - `price_id` becomes the new price row. + - The registration goes to `queued` with the new hash, length, commitment and `sealed`. + - `label`, `secret`, `records`, `intent_*`, the tx hashes and blocks, `reveal_after`, `reveal_before` and `error` are cleared. +- **A prepaid code instead (`/code`):** it uses the create rate limit, then checks the owner's signature and timestamp. + - **The code:** `SN` + a payload from `HMAC-SHA256(seed, "simplex.domains taken code " + registrationId)`. The page keeps it under Your names. + - **A repeat:** if `issued_code_id` already has this hash, the answer is 200 again. + - **Otherwise:** only a paid registration in `taken` qualifies. The service then: + - inserts the code (current length tier, 2 years, paid, no expiry); + - sets `issued_code_id`; + - moves the phase to `code_issued`. + - **Collisions:** a hash that another code already has is `code_conflict`. For a derived code this is unreachable in practice. +- **A prepaid-code order that is taken** ends there. The worker released its code at `taken`. + ## 9. Failed, retried, lapsed +A registration that cannot finish goes to `failed`. Retry checks whether it registered after all, then queues it again at no charge. The worker then decides whether the old commitment can be reused. + ```mermaid sequenceDiagram - participant Page - participant Service - participant Worker - participant DB - participant Chain - Worker->>DB: phase failed, failed_at (retries exhausted, or the commitment passed maxCommitmentAge unrevealed) - Page->>Service: POST /api/registration/:id/retry - Service->>Service: rate limit (create), phase failed? else bad_request - Service->>Chain: a register_tx of this registration (any hash) mined with status 1, or ownerOf(labelHash) = owner (a revert counts as no)? then phase registered (a lost confirmation): registered_at, expires_at, spend the prepaid code (alert the operator if released to another), done (a failed read → provider_unavailable, nothing changed) - alt a prepaid-code registration whose released code is used, revoked, expired or held elsewhere - Service-->>Page: code_used, code_invalid, code_expired or code_reserved, the registration stays failed, failed_at unchanged - Note over Page: N9 shows the code's outcome: used, held elsewhere (try later), not valid, or expired + autonumber + participant W as Webapp + participant S as Service + participant E as Ethereum node + S->>S: phase failed (retries exhausted, or not revealed in time) + W->>S: POST /api/registration/:id/retry + S->>E: did it register after all? receipts, ownerOf(labelHash) + alt it did + S->>S: phase registered + S-->>W: registered + else its prepaid code is no longer usable + S-->>W: code error, stays failed else - Service->>DB: a released code is reserved again - Service->>DB: phase queued, no charge, clear failed_at, error and the register's tx hashes and blocks, and label, secret, records, intent_* when intent_nonce ≠ SimplexSponsor.nonces(owner) + S->>S: phase queued, no charge + S-->>W: queued end - Note over Worker: from queued, the worker reads commitments(commitment): 0 (consumed by this registration already ruled out) with a commit pending → wait, else section 4, where a commit reverting UnexpiredCommitmentExists → committed with the reveal times from commitments(). Non-zero, with revealBefore = commitments(commitment) + maxCommitmentAge (reveal_before may be null): with at least revealMargin left before revealBefore (time for the reveal and the register) → committed with the reveal times from commitments(), clearing label, secret, records, intent_* and the commit's tx hashes and blocks when the commitment's time is not the block time of a commit of ours mined with status 1 (anyone can commit a lapsed commitment again), with less → wait until a second after revealBefore and go on as after it, at it → wait a second, after it → clear label, secret, records, intent_*, error, tx hashes and blocks, reveal_after, reveal_before, then section 4, so the page signs again ``` +From `queued`, the worker reads the commitment to decide whether to commit again: + +```mermaid +sequenceDiagram + autonumber + participant S as Service + participant E as Ethereum node + S->>E: commitments(commitment) + alt 0, a commit of ours pending + S->>S: wait for it + else 0 + S->>S: commit (section 4) + else live, enough time left + S->>S: committed, reuse it + else live, too little time + S->>S: wait until a second after it lapses + else lapsed + S->>S: clear the old reveal, commit again (section 4) + end +``` + +- **Retry rules:** it uses the create rate limit. It works only from `failed`, else `bad_request`. +- **Step 1, did it register after all:** + - The answer is yes when a `register_tx` of this registration (any hash) mined with status 1. + - It is also yes when `ownerOf(labelHash)` is the owner. A reverting `ownerOf` counts as no. + - If yes, it is a lost confirmation. `registered_at` and `expires_at` are recorded, and the prepaid code is spent. If the code was released to another registration meanwhile, the operator is alerted. + - A failed chain read answers `provider_unavailable` and changes nothing. A null receipt counts as not mined. +- **The released code is no longer usable:** + - The service answers `code_used`, `code_invalid`, `code_expired` or `code_reserved`. + - The registration stays `failed`, with `failed_at` unchanged. + - N9 shows the outcome. +- **Requeue:** + - **Code:** a released code is reserved again. + - **Always cleared:** `failed_at`, `error`, and the register's tx hashes and blocks. + - **Cleared when the sponsor nonce has moved** (`intent_nonce` ≠ `SimplexSponsor.nonces(owner)`): `label`, `secret`, `records` and `intent_*`. +- **The commitment read:** + - **0** means never committed, not mined yet, or consumed. Step 1 already ruled out a registration this owner holds. + - **`revealBefore`** is `commitments(commitment)` + `maxCommitmentAge`, because `reveal_before` may be null. + - **Live, with at least `revealMargin` left:** the commitment is reused. The registration goes to `committed`, with the times from `commitments()`. + - If the commitment's time is not the block time of a commit of ours mined with status 1, a third party committed it again. + - Then `label`, `secret`, `records`, `intent_*` and the commit's tx hashes are cleared, so the page signs again. + - **At `revealBefore` itself:** the worker waits a second. + - **After `revealBefore`:** the worker first clears `label`, `secret`, `records`, `intent_*`, `error`, the tx hashes and blocks, `reveal_after` and `reveal_before`. Then it commits again. +- **Failure causes:** `InsufficientAllowance` also alerts the operator. + ## 10. Restore with the owner code +In another browser, or after losing the order link, the owner code alone finds and reopens the order. + ```mermaid sequenceDiagram - participant Page - participant Service - participant DB - alt order link known (N7b) - Page->>Service: GET /api/invoice/:id or /api/registration/:id (read limit) + autonumber + participant W as Webapp + participant S as Service + alt the order link is known (N7b) + W->>S: GET /api/invoice/:id or /api/registration/:id else only the code (Your names) - Page->>Page: walletFromCode(code), timestamp - Page->>Page: signature = personal_sign("simplex.domains find " + owner (lowercase 0x hex) + " " + timestamp (Unix seconds)) - Page->>Service: POST /api/registration/find {owner, timestamp, signature} - Service->>Service: rate limit (find), recover signer = owner, timestamp within 10 minutes - Service->>DB: registrations by owner, retargeted ones included - alt none - Service-->>Page: 404 not_found - else - Service-->>Page: {registrations: [{registrationId, invoiceId or null, phase, createdAt}]}, newest first - Page->>Page: open the first not unpaid, else the first - Page->>Service: GET /api/invoice/:invoiceId when set (the order page, with what was paid), else GET /api/registration/:id (read limit) - end + W->>W: walletFromCode(code) + W->>W: sign "simplex.domains find owner timestamp" + W->>S: POST /api/registration/find (owner, timestamp, signature) + S-->>W: registrations, newest first + W->>S: GET the newest that is not unpaid end - Page->>Page: walletFromCode(code), owner = registration's owner? - Page->>Page: open sealed → label, records, secret, commitment = registration's commitment? - alt phase committed and not revealed - Page->>Service: reveal (section 5) - else phase code_issued - Page->>Page: derive the taken code from the seed and the registration id, show it, store it under Your names + S-->>W: the registration + W->>W: check the owner, open sealed → name, links, secret + alt committed, not revealed + W->>S: reveal (section 5) + else code_issued + W->>W: derive the taken code, keep it under Your names else - Page->>Page: show the phase, or the registered name + W->>W: show the phase or the name end ``` +- **The `find` signature:** EIP-191 (`personal_sign`) over `simplex.domains find `. + - `owner` is lowercase `0x` hex. + - `timestamp` is in Unix seconds. +- **`find` checks:** its own rate limit. The signer must be `owner`, and the timestamp must be within 10 minutes. +- **`find` answers:** + - `{registrations: [{registrationId, invoiceId or null, phase, createdAt}]}`, including retargeted ones; + - `404 not_found` when there are none. +- **Opening a registration:** with the read limit, the page opens: + - `GET /api/invoice/:invoiceId` when it is set (the order page, with what was paid); + - else `GET /api/registration/:id`. +- **Checking the code:** the page rebuilds the commitment from `sealed` and the seed. It must equal the registration's commitment. + ## 11. Links: preview at checkout, edit later +After registration, the owner changes links by signing each edit, and the service relays it and pays the gas. + +At checkout, a link is previewed before it goes into the registration: + ```mermaid sequenceDiagram - participant Page - participant Service - participant DB - participant Chain - Page->>Service: POST /api/link/preview {link} - Service->>Service: rate limit (preview, its own) - Service->>Service: parse a SimpleX contact or channel link, within 4096 bytes, linkProfile(link) (APIConnectPlan ‡) - Service-->>Page: {type, displayName, image} or invalid_link - Note over Page: the links go into registration.data at checkout (section 2) - Note over Page: a key holds its links joined with #59;, so removing one of several rewrites the value, and clearing a key sets "". To clear all, the page signs ClearRecords and the service relays clearRecordsWithSig - loop each changed key (N12b) - Page->>Page: no Edit links when the answer has an owner other than the code's address (redeemed) or an owner and null records (another resolver or none), else the values from the answer's records or, during the lag (Edit links only once the view is registered), the order's links in sealed, or this browser's last mined edit of that key (a mined clear sets both to ""), with the chain id and resolver from the view, and nonce = recordsNonce from the search answer or the registration view, +1 after each {tx}, the same after record_reverted, re-read after any bad_request, deadline = now + 1 hour, SetText(node, key, value, nonce, deadline) under the SimplexResolver domain, sign - Page->>Service: POST /api/name/records {labelHash, key, value, nonce, deadline, signature} - Service->>Service: rate limit (records, its own, every request) - Service->>Service: key simplex.contact or simplex.channel, links of that kind within 4096 bytes (invalid_link) - Service->>Service: signer = recoverAddress †(signature, SetText or ClearRecords digest), low s? else bad_request, not counted toward the cap - Service->>DB: name_record_edits by signer and nonce - Note over Service: before any deadline check, an identical repeat (the same body on any row) waits for the same tx while pending, answers it once mined (tx null for an edit found applied with the transaction unknown), or record_reverted again for a reverted edit. Only a body on no row is new, and with a nonce whose edit is pending or mined it is bad_request. After record_reverted the nonce is free again for a new body - Service->>Service: only for a new edit: deadline ahead of the latest block (else bad_request, not counted toward the cap), per-name cap (rate_limited) - Service->>Chain: signer = ENSRegistry.owner(node), nonce = SimplexResolver.nonces(signer)? else bad_request, not counted toward the cap (a failed read → provider_unavailable) - Service->>DB: insert the pending edit (signer, nonce, body_hash), unique among pending and mined rows: a concurrent twin loses here, before any signing - Service->>Service: enqueue on the registrar's one sender (the worker's), which assigns the key's nonce and signs - Service->>DB: the signed transaction's hash and raw bytes, saved before sending - Service->>Chain: send setTextWithSig(node, key, value, nonce, deadline, signature), paying gas, until mined - Service->>DB: the edit's state, mined or reverted - Note over Service: on any revert (an applied edit reverts InvalidNonce, or SignatureExpired once its deadline has passed) or a restart's expiry, the service re-reads nonces(signer) and the record: nonce past this edit's and the record holding its value (for a clear, both keys "") answers mined with tx null, otherwise record_reverted. A restart that finds the edit's registrar nonce used with no receipt does the same read: applied answers mined with tx null, otherwise the edit is signed again under a new registrar nonce while its deadline is ahead, then the expiry rule applies - Service-->>Page: {tx}, or record_reverted with the revert's name when known (a mined revert: SignatureExpired past the deadline, else null) + autonumber + participant W as Webapp + participant S as Service + W->>S: POST /api/link/preview (link) + S->>S: SimpleX contact or channel link? APIConnectPlan + S-->>W: type, displayName, image (or invalid_link) + Note over W: the links go into the registration's data (section 2) +``` + +Later, editing a registered name's links: + +```mermaid +sequenceDiagram + autonumber + participant W as Webapp + participant S as Service + participant E as Ethereum node + W->>W: sign SetText(node, key, value, nonce, deadline) with the owner key + W->>S: POST /api/name/records (labelHash, key, value, nonce, deadline, signature) + S->>S: recover the signer, check the link and a repeat + S->>E: owner(node), nonces(owner) + S->>S: save the pending edit + S->>E: SimplexResolver.setTextWithSig(…), the service pays gas + E-->>S: receipt + S-->>W: tx, or record_reverted +``` + +When an edit reverts, or a restart finds it unresolved: + +```mermaid +sequenceDiagram + autonumber + participant S as Service + participant E as Ethereum node + S->>E: nonces(signer), text(node, key) + alt nonce passed and the record holds the value + S->>S: mined (applied by another transaction): answer tx null + else deadline still ahead (restart, nonce used, no receipt) + S->>S: sign again with a new registrar nonce + else + S->>S: reverted: answer record_reverted end ``` +- **Preview:** its own rate limit. The link must be within 4096 bytes. +- **How records are stored:** + - A key holds its links joined with ";". + - Removing one of several links rewrites the whole value. + - Clearing a key sets it to "". +- **Clearing all records:** the page signs `ClearRecords` instead, and the service relays `clearRecordsWithSig`. +- **When the page offers Edit links:** + - **Hidden** when the search answer has an owner other than the code's address (the name was redeemed). + - **Hidden** when the answer has an owner with null records (another resolver, or none). + - **The current values** come from the answer's records. + - **During the lag**, the values come from the order's links in `sealed`, or from this browser's last mined edit of that key. A mined clear sets both keys to "". The chain id and resolver come from the view. Edit links is offered only once the phase is `registered`. +- **The page's nonce:** + - **Start:** `recordsNonce` from the search answer or the view. + - **Next nonce:** +1 after each `{tx}`. The same after `record_reverted`. + - **Recovery:** re-read it after any `bad_request`. + - **Deadline:** now + 1 hour. +- **The service's checks, in order:** + 1. **Rate limit:** its own, for every request. + 2. **The record:** the key is `simplex.contact` or `simplex.channel`. Its links are of that kind and within 4096 bytes, else `invalid_link`. + 3. **The signature:** recover the signer (`recoverAddress` †) and require low s. Otherwise `bad_request`, not counted toward the cap. + 4. **A repeat:** found by signer and nonce, and matched by `body_hash`. This runs before any deadline check. + - An identical repeat waits for the same tx. It answers the tx once mined (tx null when another transaction applied it), or answers `record_reverted` again. + - A different body with a nonce whose edit is pending or mined is `bad_request`. + - After `record_reverted`, the nonce is free again for a new body. + 5. **For a new edit only:** the deadline must be ahead of the latest block, else `bad_request`. Then the per-name cap applies (`rate_limited`). + 6. **On chain:** the signer must be `ENSRegistry.owner(node)`, and the nonce must equal `SimplexResolver.nonces(signer)`, else `bad_request`. A failed read is `provider_unavailable`. + 7. **Concurrency:** the pending row (signer, nonce, body hash) is inserted before signing. Of two concurrent identical requests, one loses the unique insert. + 8. **Sending:** through the registrar's one sender. The hash and raw tx are saved before sending. +- **The revert re-read:** the service re-reads the nonce and the record in these cases: + - after a revert: an applied edit reverts `InvalidNonce`, or `SignatureExpired` once past its deadline; + - after a restart's expiry. + - **The revert's name:** a mined revert's receipt has no data. It is named `SignatureExpired` when past the deadline, else null. + ## 12. Redeem in the app +The owner moves the name from the owner wallet into the app's wallet, so the app holds it without the owner code. + ```mermaid sequenceDiagram - participant App - participant Service - participant Chain - App->>App: parseNameCode(text) → NCOwner, walletFromCode → code key and address - App->>Chain: balanceOf, tokenOfOwnerByIndex, labelOf, nameExpires on the base registrar - App->>App: ApproveName(from = code address, to = app address, tokenId, empty key, zero tag, nonce, deadline), signed with the code key - App->>Service: relay the approval - Service->>Chain: BaseRegistrar.approveWithSig, paying gas, until mined - App->>Chain: offers(tokenId), incremented by that approval - App->>App: ClaimName(from, to, tokenId, offer, nonce, deadline), signed with the app wallet's key - App->>Service: relay the claim (endpoints not specified: gap 9) - Service->>Chain: BaseRegistrar.claimWithSig, paying gas - Chain-->>App: the token and the registry node are the app wallet's, the code no longer controls them + autonumber + participant A as SimpleX app + participant S as Service + participant E as Ethereum node + A->>A: parseNameCode → NCOwner, walletFromCode → key, address + A->>E: balanceOf, tokenOfOwnerByIndex, labelOf, nameExpires + A->>A: sign ApproveName with the code key + A->>S: relay the approval + S->>E: BaseRegistrar.approveWithSig, the service pays gas + A->>E: offers(tokenId) + A->>A: sign ClaimName with the app wallet's key + A->>S: relay the claim + S->>E: BaseRegistrar.claimWithSig, the service pays gas + E-->>A: the name and its registry node now belong to the app wallet ``` +- **`ApproveName`:** from the code address to the app address, with `tokenId`, an empty key, a zero tag, a nonce and a deadline. +- **`ClaimName`:** from, to, `tokenId`, the offer, a nonce and a deadline. The approval increments `offers(tokenId)`. +- **The relay endpoints** are not specified yet (gap 9). +- **Afterwards** the owner code no longer controls the name. + ## 13. Opened from the app +The app opens the store for a name that its own wallet will own, so there is no owner code. The link is `simplex.domains/#/?name=