mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-09-27 15:48:54 +00:00
replace announcements with a seed-addressed contact queue
This commit is contained in:
@@ -12,8 +12,8 @@ alone, on a device with no chat database, because that is the only recovery
|
||||
[wallet keys](./2026-09-10-wallet-keys.md) leaves them.
|
||||
|
||||
This adds the keys and the flow for both: sending a name to someone with no
|
||||
handshake, receiving one without being bound to it, and rediscovering received
|
||||
names from the phrase. Nothing here is implemented yet. The same mechanism
|
||||
handshake, receiving one without being bound to it, and finding received names
|
||||
again from the phrase. Nothing here is implemented yet. The same mechanism
|
||||
carries anything else an address can hold, which is why the keys sit where they
|
||||
do.
|
||||
|
||||
@@ -30,7 +30,6 @@ can recognise:
|
||||
r random, R = r*G
|
||||
s = keccak256(x || y of r*P_view) the point, SEC1 prefix dropped
|
||||
destination = addr(P_spend + s*G)
|
||||
view tag = s[0]
|
||||
```
|
||||
|
||||
The recipient recovers the same point from the other side, and with it the key:
|
||||
@@ -41,20 +40,19 @@ their key for it = (p_spend + s') mod n
|
||||
```
|
||||
|
||||
`Simplex.Messaging.Eth.Stealth` in the pinned simplexmq already implements this
|
||||
(`metaAddress`, `stealthDestination`, `stealthMatch`, `stealthPrivateKey`),
|
||||
with the encodings pinned to the EIP author's reference implementation and
|
||||
tested against an independent one. It needs no build flag: the module
|
||||
multiplies points directly rather than calling `secp256k1_ecdh`, whose built-in
|
||||
hash is SHA-256 where ERC-5564 wants keccak256. Nothing new is needed one layer
|
||||
down.
|
||||
(`metaAddress`, `stealthDestination`, `stealthPrivateKey`), with the encodings
|
||||
pinned to the EIP author's reference implementation and tested against an
|
||||
independent one. It needs no build flag: the module multiplies points directly
|
||||
rather than calling `secp256k1_ecdh`, whose built-in hash is SHA-256 where
|
||||
ERC-5564 wants keccak256. Nothing new is needed one layer down.
|
||||
|
||||
**The destination key is not at a derivation path.** It is the spending key
|
||||
tweaked by a scalar that only `R` and the viewing key produce. So the wallet
|
||||
has to hold a key as "spending key plus ephemeral public key" rather than as a
|
||||
path, which is what the table in the schema section stores. That is also why
|
||||
the stealth keys belong to a profile and not to a name: a meta-address is an
|
||||
identity published once, while a name is a thing that arrives many times, each
|
||||
on its own tweaked key.
|
||||
path, which is what the table in the schema section stores and what the backup
|
||||
carries. That is also why the stealth keys belong to a profile and not to a
|
||||
name: a meta-address is an identity published once, while a name is a thing
|
||||
that arrives many times, each on its own tweaked key.
|
||||
|
||||
## Derivation
|
||||
|
||||
@@ -81,6 +79,16 @@ people hold assets at it.
|
||||
`0'` for spend and `1'` for view is BIP-352's convention, reused so that one
|
||||
mental model covers every chain.
|
||||
|
||||
The backup in its own section needs keys that are not secp256k1, so they do not
|
||||
come from a BIP-32 path. They come from one secret:
|
||||
|
||||
```
|
||||
backup secret = HKDF(bip39 seed, "SimpleX wallet backup"), 32 bytes
|
||||
```
|
||||
|
||||
Everything the backup queue needs is derived from that secret, and nothing else
|
||||
about it may ever change, for the same reason as a path.
|
||||
|
||||
**This reintroduces the profile account that wallet keys removed**, and for the
|
||||
opposite reason. Names were moved to a device counter because which profile
|
||||
owns a name is in the record, not in the key, so a profile dimension there
|
||||
@@ -88,15 +96,13 @@ carried a mapping nothing read. A meta-address is different: it is published
|
||||
per profile and it is what tells two profiles apart, so the dimension is load
|
||||
bearing.
|
||||
|
||||
**One meta-address per device is rejected.** It would collapse the recovery
|
||||
scan to one pass, but the viewing key is published inside the meta-address, so
|
||||
any contact who saw two of your profiles could link them. The same argument
|
||||
rejects sharing one viewing key across accounts.
|
||||
**One meta-address per device is rejected.** The viewing key is published
|
||||
inside the meta-address, so any contact who saw two of your profiles could link
|
||||
them. The same argument rejects sharing one viewing key across accounts.
|
||||
|
||||
**Indices are allocated densely and never reused.** A profile takes one when it
|
||||
first publishes a meta-address, not when it is created, so the live set is
|
||||
`1..N` with holes only where profiles were deleted. Dense allocation is what
|
||||
makes the gap limit in the recovery section work.
|
||||
`1..N` with holes only where profiles were deleted.
|
||||
|
||||
## What the meta-address discloses
|
||||
|
||||
@@ -115,13 +121,12 @@ against the counterparty. And **the relayer sees the destination**, so
|
||||
unlinkability holds against chain analysis, not against SimpleX.
|
||||
|
||||
Publication is opt-in per profile, and follows `contactDomain`: a field on
|
||||
`Profile`, carried to group members through `redactedMemberProfile` and
|
||||
**gated on `allowDirect` the same way**. That refines the earlier plan, which
|
||||
passed it through
|
||||
unconditionally so that gifting would work for people known only through a
|
||||
group. Gating loses nothing: a member who cannot direct message you has no
|
||||
channel to tell you the ephemeral key either, so the gift would be inert
|
||||
anyway.
|
||||
`Profile`, carried to group members through `redactedMemberProfile` and **gated
|
||||
on `allowDirect` the same way**. That refines the earlier plan, which passed it
|
||||
through unconditionally so that gifting would work for people known only
|
||||
through a group. Gating loses nothing: a member who cannot direct message you
|
||||
has no channel to tell you the ephemeral key either, and without that channel a
|
||||
gift is unreachable, as the backup section says.
|
||||
|
||||
**ERC-6538 is not used.** The on-chain meta-address registry buys nothing here.
|
||||
The profile already distributes the meta-address to exactly the people who can
|
||||
@@ -131,53 +136,49 @@ out of something with no reason to be public.
|
||||
## Sending
|
||||
|
||||
The transfer is the registrar's sponsored path, signed by the name's key and
|
||||
submitted by the relayer, with the ephemeral key and view tag inside the signed
|
||||
struct:
|
||||
submitted by the relayer:
|
||||
|
||||
```solidity
|
||||
function transferWithSig(address from, address to, uint256 tokenId,
|
||||
uint256 nonce, uint256 deadline, bytes calldata sig,
|
||||
bytes calldata ephemeralPubKey, bytes1 viewTag) external;
|
||||
uint256 nonce, uint256 deadline, bytes calldata sig) external;
|
||||
```
|
||||
|
||||
```
|
||||
TransferName(address from,address to,uint256 tokenId,bytes ephemeralPubKey,bytes1 viewTag,uint256 nonce,uint256 deadline)
|
||||
TransferName(address from,address to,uint256 tokenId,uint256 nonce,uint256 deadline)
|
||||
```
|
||||
|
||||
EIP-712 domain `SimplexNames`, version `1`, `verifyingContract` the registrar.
|
||||
`ephemeralPubKey` is dynamic `bytes`, so the digest carries
|
||||
`keccak256(ephemeralPubKey)`; `viewTag` is `bytes1`, left aligned. The
|
||||
ephemeral key is the 33-byte compressed form, exactly what `stealthDestination`
|
||||
returns.
|
||||
|
||||
Signing those two fields is what makes the announcement trustworthy. Outside
|
||||
the signed struct a relayer could attach a fabricated derivation to a genuine
|
||||
transfer, or suppress the real one.
|
||||
**There is no announcement.** The registrar as written today carries the
|
||||
ephemeral key and a view tag inside the signed struct and emits ERC-5564's
|
||||
`Announcement`, so that a recipient restoring from the phrase alone can scan
|
||||
the chain for gifts. That is replaced by the backup below, and the fields and
|
||||
the event go before `.simplex` is deployed, which is possible because it is not
|
||||
yet deployed. What that buys, beyond a smaller contract: a stealth transfer is
|
||||
indistinguishable on chain from a plain one, so the chain does not even record
|
||||
that a gift happened, and the service needs no scan endpoint.
|
||||
|
||||
An empty `ephemeralPubKey` emits no announcement, so a plain transfer to an
|
||||
address the recipient gave you costs nothing extra and stays out of everyone's
|
||||
scan.
|
||||
`.testing` keeps whatever typehash it was deployed with, so the client still
|
||||
keys the signed shape off the deployment it is addressing, not off its own
|
||||
build.
|
||||
|
||||
Then the sender tells the recipient, in an ordinary chat message carrying the
|
||||
name and `R`. The view tag does not travel, because the recipient recomputes
|
||||
it. The encoding of that message belongs to the names protocol and is not
|
||||
decided here.
|
||||
|
||||
**The client keys the signed shape off the deployment it is addressing, not off
|
||||
its own build.** `BaseRegistrarImplementation` is not upgradeable, so the old
|
||||
and new typehashes cannot coexist in one deployment and cannot be migrated, and
|
||||
the live `.testing` registrar keeps the old typehash and a bespoke event
|
||||
permanently. That is a fork in the client, not a migration.
|
||||
The sender tells the recipient in an ordinary chat message carrying the name
|
||||
and `R`. **The message goes first.** It is handed to the agent, which persists
|
||||
it, before the transfer is signed and submitted, so a sender that dies in
|
||||
between leaves a message and no transfer, which is harmless, rather than a
|
||||
transfer and no message, which would strand the name. The encoding of the
|
||||
message belongs to the names protocol and is not decided here.
|
||||
|
||||
## Receiving
|
||||
|
||||
**The message is not trusted.** The client derives the address itself from `R`
|
||||
and its own keys, and confirms on chain that the name is held there. A sender
|
||||
cannot name a gift something it is not.
|
||||
cannot name a gift something it is not, and a message whose transfer never
|
||||
landed is dropped after a while.
|
||||
|
||||
Only then does the destination become a row, with `accepted_at` null.
|
||||
**Nothing is claimed, displayed as yours, or attached to a profile until the
|
||||
user accepts.** An unaccepted name is not yours.
|
||||
Only then does the destination become a row, with `accepted_at` null. **Nothing
|
||||
is claimed, displayed as yours, or attached to a profile until the user
|
||||
accepts.** An unaccepted name is not yours.
|
||||
|
||||
Declining deletes the row. It writes nothing on chain and leaves no trace,
|
||||
which is the whole point: acceptance is the act that creates the public link,
|
||||
@@ -199,52 +200,101 @@ against the node: the contracts no longer grant or count them, and
|
||||
A free gift is therefore not free to use, and the screen has to say so before
|
||||
the purchase sheet opens rather than after.
|
||||
|
||||
## Recovery from the phrase alone
|
||||
## Backup
|
||||
|
||||
There is **no background scan and no scheduler**. A sender can only derive the
|
||||
destination if they hold your meta-address, and that reached them over an
|
||||
established connection, so the sender always has a channel by construction.
|
||||
Discovery is a message, not a search.
|
||||
The phrase carries entropy and nothing else. Which received names exist, and
|
||||
which indices are taken, is knowledge the device has and the phrase does not.
|
||||
The earlier plan put that knowledge on chain, as an announcement per gift, and
|
||||
rebuilt it on restore by scanning. This puts it where SimpleX already keeps
|
||||
data that has to outlive a device: the link data of a contact address queue.
|
||||
|
||||
The on-chain announcement exists for one case: a device restored from the
|
||||
phrase with no chat database, where there is no message to read. It is
|
||||
ERC-5564's `Announcement` verbatim, emitted by our registrar rather than the
|
||||
canonical singleton announcer, so a scan covers SimpleX name transfers only
|
||||
instead of every stealth transfer on the chain. That is what keeps it cheap.
|
||||
**What a contact queue offers.** Its link data is two encrypted blobs, a fixed
|
||||
one of 2 KB and a mutable one of 13.7 KB, read by `LGET` with no
|
||||
authentication, addressed by a 24 byte link ID. The client supplies that ID and
|
||||
the server does not check it (`Server.hs`, `createQueue`); it is the first 24
|
||||
bytes of an HKDF of the link key, and the encryption key is the rest
|
||||
(`Crypto/ShortLink.hs`, `contactShortLinkKdf`). Writes go through `LSET` and
|
||||
need one of the queue's recipient keys, which the client chooses at `NEW`. And
|
||||
nothing on the server expires a contact queue or its link data: there is no
|
||||
such config key, no sweep, no deletion by inactivity. A queue goes when its
|
||||
owner sends `DEL` or an operator removes it.
|
||||
|
||||
```solidity
|
||||
event Announcement(uint256 indexed schemeId, address indexed stealthAddress,
|
||||
address indexed caller, bytes ephemeralPubKey, bytes metadata);
|
||||
**The queue is derived from the backup secret.** The link key is `sha3_256` of
|
||||
the fixed data, and a reader checks that, so the fixed data has to come out
|
||||
byte for byte the same on every device that holds the seed. Everything in it
|
||||
comes from the backup secret or is a constant: the root signing key, the e2e
|
||||
key, the nonce that fixes the sender ID, the server, the entity ID, and version
|
||||
ranges pinned once and never following the client's. That gives the link ID and
|
||||
the decryption key. Writing needs more: the queue's recipient key and DH key,
|
||||
derived from the same secret, and the recipient ID, which the server assigns
|
||||
and the blob therefore carries. With that, a device holding nothing but the
|
||||
phrase derives the link ID, reads the blob, decrypts it, and can write it
|
||||
again.
|
||||
|
||||
**Where it lives.** One queue on a server of each preset operator
|
||||
(`Simplex.Chat.Operators.Presets`, SimpleX Chat and Flux today), under an agent
|
||||
user of its own, so the session it is written from shares nothing with any
|
||||
profile. Restore reads every preset server of both operators and takes the
|
||||
newest blob.
|
||||
|
||||
**What is in the blob**, in the order a restore applies it:
|
||||
|
||||
```
|
||||
version a counter, newest wins
|
||||
recipient id this queue's, on this server
|
||||
next_name_index the device's names high-water mark
|
||||
next_account_index the profile accounts high-water mark
|
||||
accounts: index, display name which account was which profile
|
||||
received: account index, chain, R, accepted
|
||||
```
|
||||
|
||||
`metadata` is `viewTag || 0x23b872dd || registrar || tokenId`, so a match
|
||||
learns what it was sent without a second lookup. **`caller` is the relayer, not
|
||||
the gifter**, and a scanner must not read it as the sender.
|
||||
Which name sits at a destination is not in it: that is on chain and the
|
||||
registrar is enumerable, and storing it would trust the sender's word. About
|
||||
forty bytes per received name leaves room for several hundred. Beyond that a
|
||||
second link key is derived the same way with an index appended, and never
|
||||
reused for anything else.
|
||||
|
||||
A scan runs on seed import and behind an explicit action, never otherwise. Per
|
||||
account it costs one point multiplication and one hash per announcement, and
|
||||
the view tag discards about 255 in 256 before the point addition. The viewing
|
||||
key never leaves the device: the service serves announcement ranges and the
|
||||
client does the arithmetic, so no delegated-scanning privacy trade arises.
|
||||
**When it is written.** On every change to any of the above, to both servers.
|
||||
The blob is padded to its fixed size, so the length never says how much is in
|
||||
it.
|
||||
|
||||
Profile accounts are walked `i = 1, 2, 3, ...` until ten consecutive accounts
|
||||
match nothing, the BIP-44 gap limit idiom, with a "scan further accounts"
|
||||
action to extend it. The walk needs no profile to exist, because each
|
||||
candidate's viewing key comes from the seed. For stealth the scan is its own
|
||||
probe, so an account that only ever received is found rather than missed. It is
|
||||
still missed if it sits behind ten accounts that received nothing, which is
|
||||
what the extend action is for.
|
||||
**What an operator sees.** A link ID nobody else ever uses, read and written at
|
||||
some times. It cannot tell whose it is, what is in it, or that it is a wallet
|
||||
at all. It can delete it, and it can serve an old version, which the counter
|
||||
makes visible and a second operator makes survivable.
|
||||
|
||||
The registrar's `TransferToSelf` revert is what keeps this bounded. Without it
|
||||
an owner could self-transfer in a loop with a fresh ephemeral key for the price
|
||||
of gas and inflate every recipient's scan without limit.
|
||||
**What this rests on**, said plainly: availability is now the operators', not
|
||||
Ethereum's. No code, config or policy promises to keep a queue for years; the
|
||||
server's own design notes contemplate expiring contact queues after about three
|
||||
years of inactivity, unimplemented today. A live client touches its backup on
|
||||
every change, and reading it also counts as activity on the server, so a device
|
||||
that is in use keeps it alive. A device that is dead for years relies on the
|
||||
operators having kept it. That is the trade for having no public marker per
|
||||
gift, no scan, and a restore that also brings back which account was which
|
||||
profile, which no scan could.
|
||||
|
||||
Two gaps remain, both benign. The transfer and the message are not atomic, so a
|
||||
sender can submit and crash before sending. And a contact can forward your
|
||||
meta-address to someone you have no channel with, who can then send without
|
||||
being able to tell you. In both cases nothing is lost, because the name sits at
|
||||
an address the seed already controls and a rescan finds it, and nothing is
|
||||
attached without consent.
|
||||
**When the backup is gone**, two things still work. Names bought on this device
|
||||
are found by the probe that wallet keys deferred to the registrar: `balanceOf`
|
||||
over `k = 1, 2, 3, ...` until ten in a row own nothing, which also sets
|
||||
`next_name_index`. And received names come back from their senders, next.
|
||||
|
||||
## Senders keep what they sent
|
||||
|
||||
The sender always knows `R`. So the sender keeps every gift it sent, keyed by
|
||||
the verified name of the contact it went to, and when a contact verified under
|
||||
that name connects again, it sends them all again. A restored recipient is a
|
||||
new contact, not the old row, which is why the key is the name and not the
|
||||
contact. The recipient verifies each one as it would any message, and ignores
|
||||
what it already has or cannot use.
|
||||
|
||||
This costs nothing and covers the case the backup cannot: a device dead long
|
||||
enough for the operators to have dropped the queue. It reaches only recipients
|
||||
who own a bought name, because that is what lets a contact find them again, and
|
||||
only gifts whose sender is still around. It is a supplement, not a path.
|
||||
|
||||
A gift sent to a forwarded meta-address, by someone with no channel to the
|
||||
recipient, is unsupported. It is not lost, because the address is one the seed
|
||||
controls, but nothing will tell the recipient it is there.
|
||||
|
||||
## Other assets
|
||||
|
||||
@@ -262,8 +312,8 @@ m/44'/128'/i'/0' Monero, then SHA3 and sc_reduce32 to spend and view
|
||||
```
|
||||
|
||||
Nothing is designed for them here and no storage anticipates them beyond the
|
||||
`chain` column below. What is being fixed is the path layout, because that is
|
||||
the one thing later work cannot change.
|
||||
`chain` column below and the chain tag in the blob. What is being fixed is the
|
||||
path layout, because that is the one thing later work cannot change.
|
||||
|
||||
## Schema
|
||||
|
||||
@@ -272,22 +322,20 @@ Postgres lists.
|
||||
|
||||
```sql
|
||||
-- High-water mark for profile accounts. It cannot be read off users: after a
|
||||
-- phrase-only restore that table is empty while accounts already hold keys, so
|
||||
-- the next profile would republish a recovered account's meta-address. The scan
|
||||
-- sets this, and new profiles allocate above it.
|
||||
-- restore that table is empty while accounts already hold keys, so the next
|
||||
-- profile would republish a recovered account's meta-address. The backup
|
||||
-- restores it, and new profiles allocate above it.
|
||||
ALTER TABLE wallet_seeds ADD COLUMN next_account_index INTEGER NOT NULL DEFAULT 1;
|
||||
|
||||
-- Resume point of the last scan, so a repeat scan is not a rescan from zero.
|
||||
-- Not a live watermark: normal discovery is a message.
|
||||
ALTER TABLE wallet_seeds ADD COLUMN scanned_to TEXT;
|
||||
-- The blob's counter as last written, so a restore knows what is newer.
|
||||
ALTER TABLE wallet_seeds ADD COLUMN backup_version INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
ALTER TABLE users ADD COLUMN wallet_account_index INTEGER;
|
||||
|
||||
ALTER TABLE contact_profiles ADD COLUMN stealth_meta_address BLOB;
|
||||
|
||||
-- Destinations learned from a message or rediscovered by a scan. The chain is
|
||||
-- carried from the start so that BTC and XMR need no migration, only rows with
|
||||
-- a different value.
|
||||
-- Destinations learned from a message or restored from the backup. The chain
|
||||
-- is carried from the start so that BTC and XMR need no migration, only rows
|
||||
-- with a different value.
|
||||
CREATE TABLE wallet_destinations (
|
||||
wallet_destination_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
wallet_seed_id INTEGER NOT NULL REFERENCES wallet_seeds ON DELETE RESTRICT,
|
||||
@@ -302,17 +350,27 @@ CREATE TABLE wallet_destinations (
|
||||
|
||||
CREATE INDEX idx_wallet_destinations_account
|
||||
ON wallet_destinations(wallet_seed_id, account_index);
|
||||
|
||||
-- What this device sent, so it can send it again. Keyed by the recipient's
|
||||
-- verified name, because that is what a restored recipient is found by.
|
||||
CREATE TABLE wallet_sent (
|
||||
wallet_sent_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
contact_domain TEXT NOT NULL,
|
||||
chain TEXT NOT NULL,
|
||||
ephemeral_pub_key BLOB NOT NULL,
|
||||
sent_at TEXT NOT NULL
|
||||
) STRICT;
|
||||
```
|
||||
|
||||
**Destinations hang off the seed and an account index, not off a user.** The
|
||||
earlier plan keyed them on `users`, which cannot hold what a scan finds: after
|
||||
a phrase-only restore an account has keys and received names while no profile
|
||||
exists yet. `users.wallet_account_index` is the join, so adopting a recovered
|
||||
account for a new profile attaches its destinations with no row rewriting.
|
||||
`scanned_to` moves onto the seed for the same reason.
|
||||
earlier plan keyed them on `users`, which cannot hold what a restore brings
|
||||
back: an account has keys and received names while no profile exists yet.
|
||||
`users.wallet_account_index` is the join, so adopting a recovered account for a
|
||||
new profile attaches its destinations with no row rewriting.
|
||||
|
||||
No private key is stored. `ephemeral_pub_key` is what re-derives one from the
|
||||
seed, so the table is a cache: losing it costs a rescan, not an asset.
|
||||
No private key is stored, here or in the blob. `ephemeral_pub_key` is what
|
||||
re-derives one from the seed, so the table is a cache: losing it costs a
|
||||
restore, not an asset.
|
||||
|
||||
`ON DELETE RESTRICT` changes what `/_wallet delete` does: it starts failing
|
||||
once a destination exists, rather than destroying control of the names at them.
|
||||
@@ -325,9 +383,9 @@ was received and is the only copy. For a profile of your own it is derivable
|
||||
from the seed and the account index, and stored only so that one column serves
|
||||
both.
|
||||
|
||||
**Which name is at a destination is not stored**, because it is on chain and
|
||||
the registrar is enumerable. Storing it would mean trusting the sender's word
|
||||
for it.
|
||||
The backup queue itself is an ordinary connection in the agent database. What
|
||||
is new is that its keys are derived rather than drawn, which is the dependency
|
||||
named under scope.
|
||||
|
||||
## Commands
|
||||
|
||||
@@ -340,13 +398,15 @@ it returns, so stealth follows that shape.
|
||||
/_wallet meta off stop publishing it
|
||||
/_wallet send <name> <meta> derive a destination and sign the transfer
|
||||
/_wallet received destinations for this profile's account
|
||||
/_wallet scan rediscover destinations from announcements
|
||||
/_wallet backup write the blob now, and say where it went
|
||||
/_wallet restore read the blob and apply it
|
||||
/_wallet export stealth <id> one destination's secret, 0x and 64 hex digits
|
||||
```
|
||||
|
||||
Reading never allocates. `meta on` is the opt-in and the only thing that takes
|
||||
an account index, matching the rule that a seed is never created as a side
|
||||
effect of reading.
|
||||
effect of reading. `backup` exists so the write can be driven and checked; in
|
||||
use the write is automatic.
|
||||
|
||||
`export stealth` returns an ordinary secp256k1 key that MetaMask imports, and
|
||||
it discloses that one address and nothing else: not the seed, not the other
|
||||
@@ -363,44 +423,55 @@ feature. This is a positive check, not an omission to rely on.
|
||||
Hidden profiles are as wallet keys left them: they should not register a name,
|
||||
and they should not publish a meta-address either. The seed is the device's, so
|
||||
whoever unlocks any profile can derive every profile's stealth keys, hidden
|
||||
ones included.
|
||||
ones included, and the backup lists every profile's account by name.
|
||||
|
||||
## What the phrase does not carry
|
||||
|
||||
The phrase carries entropy. Which profile accounts were taken is not in it, and
|
||||
`next_account_index` starts at 1 after an import, so until a scan raises it a
|
||||
new profile would publish a meta-address that a recovered account already
|
||||
published. Nothing can act on the stale mark until publishing exists, and the
|
||||
scan lands with it.
|
||||
The phrase carries entropy. The backup carries the rest, and the phrase is what
|
||||
finds the backup. With both, a restore is complete: indices, accounts, which
|
||||
profile each was, and every received name, whether or not its message ever
|
||||
arrived.
|
||||
|
||||
Which profile an account was is gone with the chat database, and what an
|
||||
account received is the only thing left that identifies it. Names bought on
|
||||
this device do not help: they are counted per device and hang off no account at
|
||||
all.
|
||||
With the phrase alone, bought names come back by the probe and received names
|
||||
only from their senders. Which profile an account was is gone.
|
||||
|
||||
## Risks
|
||||
|
||||
- Stealth does not protect against the sender, who can prove the derivation.
|
||||
- Unlinkability is against chain observers, not against SimpleX: the relayer sees
|
||||
destinations and later relays intents from them.
|
||||
- Anyone who can direct message you can send you a name. Spam surface, bounded by
|
||||
the sender paying to register.
|
||||
- Unlinkability is against chain observers, not against SimpleX: the relayer
|
||||
sees destinations and later relays intents from them.
|
||||
- The backup's availability rests on the preset operators. Nothing promises
|
||||
years, and a device dead long enough loses it. Two operators and the senders
|
||||
are the mitigations, not a guarantee.
|
||||
- The backup is a map of everything the seed controls, in one place. It is
|
||||
encrypted under a key only the seed produces and padded to a fixed size, so
|
||||
what is exposed is that a blob exists and when it changes.
|
||||
- Anyone who can direct message you can send you a name. Spam surface, bounded
|
||||
by the sender paying to register.
|
||||
- Accepting a gift costs money, so a free gift is not free to use.
|
||||
- Received names need our software or a reimplementation: they sit at addresses
|
||||
that are at no derivation path, announced by our own registrar, so no
|
||||
third-party wallet finds them from a phrase. `export stealth` keeps the
|
||||
per-name guarantee true, but one address at a time.
|
||||
- A crash between transfer and message loses the notification, though not the
|
||||
name. The recipient hears nothing until the sender resends or a scan runs.
|
||||
- The derivation paths cannot be changed after anyone holds an asset at one.
|
||||
- Received names need our software or a reimplementation: they sit at
|
||||
addresses that are at no derivation path, findable only through the backup
|
||||
or the sender, so no third-party wallet finds them from a phrase.
|
||||
`export stealth` keeps the per-name guarantee true, but one address at a
|
||||
time.
|
||||
- The registrar changes shape before deployment. What is built and tested
|
||||
today carries the announcement; removing it is work in the contracts, and
|
||||
`.simplex` must not deploy with it in.
|
||||
- The derivation paths and the backup secret cannot be changed after anyone
|
||||
holds an asset at one.
|
||||
|
||||
## Scope
|
||||
|
||||
Here: the stealth keys and their paths, the profile account, meta-address
|
||||
publication, the send and receive flows, the recovery scan, and the storage all
|
||||
of that needs.
|
||||
publication, the send and receive flows, the backup and what it carries, the
|
||||
senders' re-send, and the storage all of that needs.
|
||||
|
||||
Not here: buying a name, the names protocol and the encoding of the message
|
||||
that carries the ephemeral key, the registrar and relayer, the service that
|
||||
serves announcement ranges, the top-up purchase, any UI, several seeds per
|
||||
that carries the ephemeral key, the registrar and relayer, the contract change
|
||||
that removes the announcement, the top-up purchase, any UI, several seeds per
|
||||
device, a profile's own addresses, Bitcoin, Monero and ERC-20.
|
||||
|
||||
Two changes in simplexmq are a dependency and are not designed here: a way to
|
||||
create a contact link from supplied keys and nonce rather than from the agent's
|
||||
random generator (`prepareConnectionLink'` draws both), and a way to register a
|
||||
queue that already exists on the server, for the device that restores.
|
||||
|
||||
Reference in New Issue
Block a user