diff --git a/CHANGELOG.md b/CHANGELOG.md index 15f3eafd7..758cf8758 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Crypto: SMP protocol: - `ROWN` command (v23): the names an address owns, and whether the account has been used at all, so a recovered wallet seed can be scanned for the accounts - already in use. + already in use. See `plans/2026-09-22-names-owned-by.md`. # 6.5.1 diff --git a/plans/2026-09-22-names-owned-by.md b/plans/2026-09-22-names-owned-by.md new file mode 100644 index 000000000..92a939d4b --- /dev/null +++ b/plans/2026-09-22-names-owned-by.md @@ -0,0 +1,78 @@ +# Owned-by: the names an address owns + +A recovered seed does not say how many of its accounts have been used. Until it +does, a restored device cannot take a fresh account without risking one its +owner already has, and cannot find the names it already holds. This adds the +question the protocol could not ask: what does this address own. + +This is Workstream B of the SimpleX names v2 plan, after the Ethereum crypto +primitives. The client consumer is the wallet recovery scan in simplex-chat. + +## What this delivers + +``` +GET /v2/owned-by/
?offset=N resolver, JSON OwnedNames +ROWN
-> ROWND SMP v23 (nameOwnedSMPVersion) +ownedNames server, agent client, agent +``` + +The agent returns the relay it used alongside the answer, so a caller can pass +it back as used and ask the next account elsewhere. Sending every account of a +seed to one relay would tell that relay the accounts belong to one wallet; the +scan therefore asks one account per relay, and `getNextNameServer` avoids the +hosts already used where the configured set allows. `proxiedSMPRelayVersion` is +pinned to the current version so ROWN can be proxied at all — a scan that falls +back to a direct session hands the relay its address with its IP. + +Enumeration is read off the ERC-721 registrar (`balanceOf`, +`tokenOfOwnerByIndex`, `labelOf`), not from registration logs: the token is the +name, so a name acquired by transfer counts. The registrar does not maintain +enumeration on expiry — it states this as an invariant, and burns a token only +when the name is re-registered — so a lapsed name stays listed. It is reported +with its `status` rather than filtered, because a caller scanning a recovered +seed is exactly the caller who needs to be told a name can still be renewed. + +## What `inUse` actually answers + +`inUse` is `holds a name now, or nonce > 0, or balance > 0`, on the one chain +the resolver is configured with. Holding a name is only one way for an account +to be in use: an account that was funded, or ever sent a transaction, is in use +with no name, and a scan that reads names alone hands out an account its owner +is already using. Within that scope the signal is sound — an EOA's balance can +only fall through its own transaction, which bumps the nonce, so "funded then +drained" still reads as used, and an EIP-7702 delegation leaves the authority's +nonce incremented, so it needs no separate code check. + +It does not see: + +- **Accounts holding only other tokens.** An EOA can hold ERC-20s or NFTs with + nonce 0 and no ether. ERC-20 has no reverse index, so answering this needs + `eth_getLogs` over Transfer topics for all history, which providers cap, or an + external indexer. Out of scope here. +- **Accounts used on another chain.** The same key is the same address on every + L2 and sidechain; this chain's nonce and balance stay 0. +- **History, as opposed to current state.** `balanceOf` is present ownership. A + name registered for an account by a controller — which leaves the owner at + nonce 0 — and later transferred away, or lapsed and re-registered by someone + else, leaves no trace. + +The cost is bounded. A name-holding account is always found, so no account that +holds a name is ever mislabelled. A missed account matters only as a gap-limit +reset: the scan can stop up to `scanGapLimit` indexes early and not reach a name +beyond that. That is recovery incompleteness, recoverable by scanning again on a +better signal, not loss. In the other direction a dust transfer marks an account +used; the indexes come from hardened derivation with no published xpub, so they +cannot be enumerated to be dusted. + +## What is deliberately absent + +- **No token or indexer integration**, per the limits above. +- **Nonce and balance are not carried over SMP.** The resolver reports them, and + `OwnedNames` keeps only the names, the flag and the cursor, so a client cannot + yet see why an account is in use. +- **No caching.** One owned-by is many `eth_call`s, so it is forked on the + server the same way RSLV is, and nothing is memoised. +- **Enumeration is not atomic.** `balanceOf` and each `tokenOfOwnerByIndex` are + separate calls at `latest`, so a transfer between them can duplicate or skip an + entry, and successive pages can straddle blocks. Pinning every call to one + block number is the fix if it ever matters for a recovery scan. diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 6d4f9951f..d3f32ec61 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -178,6 +178,7 @@ smpBlockSize = 16384 -- 20 - public namespaces resolver, RSLV command (6/20/2026) -- 21 - server public information in handshake (7/5/2026) -- 22 - RNAME answers name availability as well as the record (7/25/2026) +-- 23 - ROWN command, the names an address owns (9/22/2026) data SMPVersion