mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-04 15:07:32 +00:00
plans: one transaction per names links Save
This commit is contained in:
1 parent
aa6244d613
commit
158f22c7ea
1 file changed
+32
-23
@@ -81,7 +81,7 @@ In one transaction, rows are locked in this order: the invoice, then the registr
|
||||
| A link | one SimpleX short link: a contact address `https://<host>/a#…` or `simplex:/a#…`, a channel `https://<host>/c#…` or `simplex:/c#…`; at most 4096 bytes of UTF-8 | `invalid_link` |
|
||||
| Links per key | at most one `simplex.contact` and one `simplex.channel` | `invalid_link` |
|
||||
| `sealed` | at most 11095 characters | `bad_request` |
|
||||
| A request body | 16384 bytes on `/api/invoice`, `/api/name/register`, `/api/registration/:id/name`, `/spend` and `/reveal`; 8192 bytes elsewhere | `bad_request` |
|
||||
| A request body | 16384 bytes on `/api/invoice`, `/api/name/register`, `/api/registration/:id/name`, `/spend`, `/reveal` and `/api/name/records` (two links of 4096 bytes); 8192 bytes elsewhere | `bad_request` |
|
||||
|
||||
The mock reads every body up to 16384 bytes. A body the page builds is never near either cap. The page and the service check links against the same vectors (the webapp's `src/names.ts` cases, copied into the service's tests).
|
||||
|
||||
@@ -156,7 +156,7 @@ Each limit counts requests per client per minute. It is taken after the route an
|
||||
|
||||
Beyond these:
|
||||
- **Link fetches** have two service-wide bounds, one for search answers (`searchFetchSlots`, 4 at a time) and one for previews (`previewFetchSlots`, 2 at a time), so previews cannot starve search. Each fetch has a 3-second timeout (`fetchTimeout`) and never waits for a slot longer than that: it gives up, and its profile is `unread` (section 2). Profiles that were read are cached 10 minutes per link; an `unread` link is fetched again on the next request. A search reads the chain within 10 s, then its profiles within one more fetch timeout, so it answers within about 13 s.
|
||||
- **Record edits** have a per-name cap of 10 relayed edits per hour, and a yearly limit of `[names] record_edits_per_year` edits per name in any 365 days (section 10). Refused requests count toward neither.
|
||||
- **Record edits** have a per-name cap of 10 relayed requests per hour, and a yearly limit of `[names] record_edits_per_year` edits per name in any 365 days, an edit being one accepted request of one or two links (section 10). Refused requests count toward neither.
|
||||
|
||||
## 2. Search
|
||||
|
||||
@@ -218,7 +218,7 @@ A name that is paid for here but not yet `registering` (`queued` to `committed`)
|
||||
- `profile`: `ok` when the link data was read; `unread` when it could not be read now (no free slot, a timeout, a failed fetch), so searching again may fill it; `unavailable` when the service does not fetch it (section 3, the host rule). Without a profile the entity has `displayName` "", `fullName` "", `image` null, `subscribers` null, and `kind` from the link (`contact` or `channel`); the page shows the default icon.
|
||||
- The service opens each link with the chat core's connect plan (`APIConnectPlan`), which reads the link data without connecting, within the search fetch bound (section 1).
|
||||
- `owner`, `resolver`: the registry's `owner(node)` and `resolver(node)`. `recordsNonce`: `SimplexResolver.nonces(owner)`, for the next edit.
|
||||
- `recordEdits`: the name's yearly limit of record edits (section 10). `left`: the edits it has left now, from 0 to `record_edits_per_year`. `nextAt`: unix seconds when `left` next grows, that is when the oldest counted edit is 365 days old (a later one while a lowered setting is overrun); null while no edit counts. `left` is 0 only with a `nextAt`. Read from the service's own store, so it costs no chain read.
|
||||
- `recordEdits`: the name's yearly limit of record edits (section 10). `left`: the edits it has left now, from 0 to `record_edits_per_year`; an edit is one Save, of one link or both. `nextAt`: unix seconds when `left` next grows, that is when the oldest counted edit is 365 days old (a later one while a lowered setting is overrun); null while no edit counts. `left` is 0 only with a `nextAt`. Read from the service's own store, so it costs no chain read.
|
||||
- `expiresAt`: the registrar's `nameExpires`; past it the name is in its grace period. `gracePeriod`: the registrar's `GRACE_PERIOD` (90 days), so the name can be registered again from `expiresAt + gracePeriod`.
|
||||
|
||||
**Registered, on another resolver contract or none:** the service reads no records from a contract it does not know.
|
||||
@@ -583,39 +583,46 @@ Only from `failed`, and not when `linksRefused` is set (its commitment fixes the
|
||||
### `POST /api/name/records`
|
||||
|
||||
```json
|
||||
{"labelHash": "0x…", "key": "simplex.channel", "value": "https://smp9.simplex.im/c#…", "nonce": 0, "deadline": 1792832400, "signature": "0x…"}
|
||||
{"labelHash": "0x…", "clear": true, "nonce": 1, "deadline": 1792832400, "signature": "0x…"}
|
||||
{"labelHash": "0x…", "edits": [
|
||||
{"key": "simplex.contact", "value": "https://smp8.simplex.im/a#…", "nonce": 4, "deadline": 1792832400, "signature": "0x…"},
|
||||
{"key": "simplex.channel", "value": "", "nonce": 5, "deadline": 1792832400, "signature": "0x…"}]}
|
||||
{"labelHash": "0x…", "edits": [{"clear": true, "nonce": 1, "deadline": 1792832400, "signature": "0x…"}]}
|
||||
```
|
||||
|
||||
Changes one record of a registered name; the service relays `setTextWithSig` (or `clearRecordsWithSig`) and pays the gas. Edit links in Your names sends one request per changed key, one at a time.
|
||||
- `key`: `simplex.contact` or `simplex.channel`. `value`: one short link of that kind, or "" to remove it. The whole record is replaced.
|
||||
- `nonce`: `recordsNonce` from the search answer, one counter for all of the owner's edits. `deadline`: signing time plus one hour.
|
||||
- The signature is `SetText` (or `ClearRecords`) with the name's key.
|
||||
One Save of Edit links: one or two signed edits of one registered name, relayed as one transaction whose gas the service pays (owner, 2026-10-29). It is all or nothing: every edit lands, or none does.
|
||||
- `edits`: 1 or 2, applied in the order sent. Each is a `SetText` (`key` and `value`) or a `ClearRecords` (`clear: true`).
|
||||
- `key`: `simplex.contact` or `simplex.channel`, each at most once in a request. `value`: one short link of that kind, or "" to remove it. The whole record is replaced.
|
||||
- `clear` goes alone, as the request's only edit. The page sends none: it removes a link with `value` "", and a Save never needs a clear beside a set, which would wipe the set or be overwritten by it.
|
||||
- `nonce`: the first edit's is `recordsNonce` from the search answer, each next one more. The resolver keeps one counter per owner, shared by all their names, and an edit is accepted only at the counter's current value, which it then raises by one (`SimplexResolver._consumeIntent`), so the second edit is valid only after the first. `deadline`: signing time plus one hour.
|
||||
- Each signature is `SetText` (or `ClearRecords`) with the name's key.
|
||||
|
||||
**The transaction.** One edit is relayed as `setTextWithSig` (or `clearRecordsWithSig`) itself. Two are relayed as the resolver's `multicall(bytes[])`, each element the calldata that edit would be sent as alone, in order. `multicall` runs each element on the resolver itself (`delegatecall` to its own address), where authority comes from the owner's signature, not from the caller; any element that fails reverts the whole transaction. It drops the failing element's error (`require(success)`), so the revert of a two-edit request has no name.
|
||||
|
||||
Checks, in order:
|
||||
1. The records limit; formats and the key; the link (`invalid_link`); the signature's format and low `s`; the signer recovered.
|
||||
2. An identical repeat of an edit that was sent (the same signed body): answered from that edit, as below, whatever the yearly limit says, and not counted again. Only sent edits are stored; a request refused before sending leaves nothing, so its repeat is checked anew.
|
||||
1. The records limit; formats: 1 or 2 edits, keys not repeated, a clear alone, nonces consecutive; the links (`invalid_link`); each signature's format and low `s`; each signer recovered, and the same for every edit.
|
||||
2. An identical repeat of a request that was sent (the same signed edits, in the same order): answered from it, as below, whatever the yearly limit says, and not counted again. Only sent requests are stored; a request refused before sending leaves nothing, so its repeat is checked anew.
|
||||
3. The yearly limit, from the store before any chain read: `429 {"error": "edit_limit", "nextAt": 1824249600}` when the name has no edit left, `nextAt` as in the search answer's `recordEdits` (the time the next edit fits). No `Retry-After`: asking again before `nextAt` cannot change it.
|
||||
4. The deadline after the latest block's time; the signer is the registry's `owner(node)`; the name's resolver is the default `SimplexResolver`; the nonce equals `SimplexResolver.nonces(owner)`. Else `bad_request`.
|
||||
5. Under the name's turn, the yearly limit again (a concurrent edit may have taken the last one), then the per-name cap: 10 relayed edits in the last hour, else `429 rate_limited` with the real wait. Only edits that were sent count, so nobody but the owner can use up a name's cap.
|
||||
4. The earliest deadline after the latest block's time; the signer is the registry's `owner(node)`; the name's resolver is the default `SimplexResolver`; the first nonce equals `SimplexResolver.nonces(owner)`. Else `bad_request`.
|
||||
5. Under the name's turn, the yearly limit again (a concurrent request may have taken the last edit), then the per-name cap: 10 relayed requests in the last hour, else `429 rate_limited` with the real wait. Only requests that were sent count, so nobody but the owner can use up a name's cap.
|
||||
6. A dry run of the transaction, as for every transaction of the registrar key. A revert sends and stores nothing, and is answered from chain state as below: applied, or `record_reverted`.
|
||||
|
||||
**The yearly limit.** Every edit is a transaction whose gas the service pays, so a name gets at most `[names] record_edits_per_year` edits (default 3, from 1 to 100, read at startup) in any 365 days, counted per label hash:
|
||||
- Each accepted request counts once, however many links it changes: a `setText` of one key, or a `clear` of both. Removing a link (`value` "") and `clear` count like any edit. Edit links sends one request per changed key, so saving both links takes two edits, and the page offers Save only while the edits left cover the change.
|
||||
**The yearly limit.** Every accepted request is a transaction whose gas the service pays, so a name gets at most `[names] record_edits_per_year` edits (default 3, from 1 to 100, read at startup) in any 365 days, counted per label hash:
|
||||
- An edit is one accepted request, however many links it changes: a Save of one link or of both, or a `clear`. Removing a link (`value` "") counts like any other change. The page offers Save while at least one edit is left, and the search answer's `recordEdits.left` counts the Saves left.
|
||||
- Links set at registration are part of the register transaction, not an edit, and do not count.
|
||||
- An edit counts from the moment it is stored to be sent (its `created_at`) while it is pending or mined, and stops counting 365 days later. A request refused before any transaction (checks 1 to 5, a dry run that reverts, or a chain that fails before the send) stores nothing and never counts.
|
||||
- A relay that fails for good gives its edit back: an edit settled `record_reverted` (a mined revert, `SignatureExpired` past its deadline, or `InvalidNonce` when another body took the nonce) stops counting when it settles. An unsent edit the lane drops is deleted and never counted. A pending edit counts until it settles, and one found applied (`{"tx": null}`, relayed by someone else) counts as mined. The per-hour cap still counts reverted edits, so a run of reverts stays bounded.
|
||||
- An edit counts from the moment it is stored to be sent (its `created_at`) while it is pending or mined, and stops counting 365 days later. A request refused before any transaction (checks 1 to 6, or a chain that fails before the send) stores nothing and never counts.
|
||||
- A relay that fails for good gives its edit back: a request settled `record_reverted` (a mined revert, `SignatureExpired` past its earliest deadline, or `InvalidNonce` when another body took a nonce) stops counting when it settles. An unsent request the lane drops is deleted and never counted. A pending request counts until it settles, and one found applied (`{"tx": null}`, relayed by someone else) counts as mined. The per-hour cap still counts reverted requests, so a run of reverts stays bounded.
|
||||
- Lowering the setting keeps the edits already counted: `left` is 0 until enough of them are 365 days old.
|
||||
|
||||
Answers:
|
||||
- `200 {"tx": "0x…"}` once mined; `{"tx": null}` when it was found applied with the applying transaction unknown (another relayer's, or ours with its receipt pruned).
|
||||
- Whether an edit was **applied** is decided from its own transactions first: one of its sent hashes with a receipt of status 1 means applied, answered with that hash. Only when no receipt is known (pruned, or relayed by someone else) is it judged by chain state: the owner's nonce moved past its nonce and the record holds its value (both keys "" for a clear) means applied, `{"tx": null}`; the nonce moved but the record holds another value means `record_reverted` with `InvalidNonce`. A later edit of the same key can make this second judgment wrong; the page's next nonce re-read corrects what it shows.
|
||||
- `409 {"error": "record_reverted", "revert": "SignatureExpired"}`: reverted, with the revert's name or null. The nonce is free again for a new body.
|
||||
- `504 edit_pending`: not mined within 90 seconds, or the chain stopped answering after the send. The edit goes on; an identical repeat waits for it again. The page says the edit may still land and re-reads the name later.
|
||||
- `200 {"tx": "0x…"}`, the request's one transaction, once mined; `{"tx": null}` when it was found applied with the applying transaction unknown (another relayer's, or ours with its receipt pruned).
|
||||
- Whether a request was **applied** is decided from its own transactions first: one of its sent hashes with a receipt of status 1 means applied, answered with that hash. Only when no receipt is known (pruned, or relayed by someone else) is it judged by chain state: the owner's nonce moved past its last nonce and every record holds its edit's value (both keys "" for a clear) means applied, `{"tx": null}`; the nonce moved past its first nonce otherwise means `record_reverted` with `InvalidNonce`. A later edit of the same key can make this second judgment wrong; the page's next nonce re-read corrects what it shows.
|
||||
- `409 {"error": "record_reverted", "revert": "SignatureExpired"}`: reverted, with the revert's name or null. None of its edits landed, and its nonces are free again for a new body.
|
||||
- `504 edit_pending`: not mined within 90 seconds, or the chain stopped answering after the send. The request goes on; an identical repeat waits for it again. The page says the change may still land and re-reads the name later.
|
||||
- `503 provider_unavailable`: the chain failed before anything was sent. Nothing is stored; the page may send it again.
|
||||
- `429 {"error": "edit_limit", "nextAt": …}`: the yearly limit is used up (check 3 or 5); nothing was sent. The page shows "You can edit links again on <date>" and keeps Save off.
|
||||
- `400 bad_request`, `400 invalid_link`, `429 rate_limited`.
|
||||
|
||||
After `{tx}` the page signs the next edit with the next nonce; after `record_reverted`, with the same one; after any `bad_request`, it re-reads `recordsNonce`.
|
||||
After `{tx}` the page signs the next Save from its nonce plus the number of edits it sent; after `record_reverted`, from the same nonce; after any `bad_request`, it re-reads `recordsNonce`.
|
||||
|
||||
## 11. Not in the API
|
||||
|
||||
@@ -674,7 +681,9 @@ Each line: the difference, the choice here, and what changes in the mock (`webap
|
||||
|
||||
41. **Late and overpaid crypto** (owner, 2026-10-27). Chosen: `cryptoAmountDue` never below `"0"`; `paidInFull` on a closed invoice from BTCPay's `PaidLate`, `PaidOver` or a due at 0 or less, its `status` staying `expired`; the extra of an overpayment credited to the order's balance; every late or extra credit the price share of the confirmed crypto at the lower rate (section 8, Balances). Mock: a payment to a closed invoice keeps it `expired` with `paidInFull` true when it covers the amount; an overpaid invoice reads `cryptoAmountDue` `"0"` and credits the extra; credits are the price share. Page: word a late payment from `paidInFull` and `registration.latePayment`, not from `status: "paid"`; accept a due of `"0"` on an overpaid invoice.
|
||||
|
||||
42. **The yearly limit of record edits** (owner, 2026-10-29). Chosen: `[names] record_edits_per_year` (default 3, 1–100) edits per name in any 365 days, counting pending and mined edits only; `recordEdits: {left, nextAt}` in the registered search answer; `429 edit_limit` with `nextAt` (section 10). Mock: the same count and refusal, its limit 3; `POST /control/edits` sets the limit (`{"limit"}`) or a name's counted edits (`{"label", "count", "at"}`) for the tests. Page: Edit links shows "N edits left this year" in one line under Save, keeps Save off while the edits left do not cover the changed links (saying so in that line), and at none, or on `edit_limit` from a tab opened earlier, "You can edit links again on <date>".
|
||||
42. **The yearly limit of record edits** (owner, 2026-10-29). Chosen: `[names] record_edits_per_year` (default 3, 1–100) edits per name in any 365 days, counting pending and mined edits only; `recordEdits: {left, nextAt}` in the registered search answer; `429 edit_limit` with `nextAt` (section 10). Mock: the same count and refusal, its limit 3; `POST /control/edits` sets the limit (`{"limit"}`) or a name's counted edits (`{"label", "count", "at"}`) for the tests. Page: Edit links shows "N edits left this year" in one line under Save, and at none, or on `edit_limit` from a tab opened earlier, "You can edit links again on <date>" with Save off.
|
||||
|
||||
43. **One transaction per Save** (owner, 2026-10-29). Chosen: `POST /api/name/records` takes `edits`, one or two signed edits of one name with consecutive nonces, relayed as one transaction (`multicall` for two), all or nothing, and counted as one edit by the yearly limit and the hourly cap; the single-edit body is gone (section 10). Mock: the new body and checks, two edits sent as `multicall`, one count per request. Page: a Save signs each changed link from `recordsNonce` up and sends them in one request; the edits line counts Saves, without "both links take 2"; a Save that fails changes no link, so no notice names a link that reached the chain.
|
||||
|
||||
## Appendix B. Error tags
|
||||
|
||||
|
||||
Reference in new issue
Block a user