Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
455 changes: 455 additions & 0 deletions plans/20260522_01_smp_public_namespaces.md

Large diffs are not rendered by default.

99 changes: 97 additions & 2 deletions protocol/simplex-messaging.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Version 19, 2025-01-24
Version 20, 2026-05-25

# Simplex Messaging Protocol (SMP)

Expand Down Expand Up @@ -67,6 +67,9 @@ Version 19, 2025-01-24
- [Queue deleted notification](#queue-deleted-notification)
- [Error responses](#error-responses)
- [OK response](#ok-response)
- [Resolver commands](#resolver-commands)
- [Resolve name command](#resolve-name-command)
- [Name record response](#name-record-response)
- [Transport connection with the SMP router](#transport-connection-with-the-SMP-router)
- [General transport protocol considerations](#general-transport-protocol-considerations)
- [TLS transport encryption](#tls-transport-encryption)
Expand All @@ -83,7 +86,7 @@ It's designed with the focus on communication security and integrity, under the

It is designed as a low level protocol for other application protocols to solve the problem of secure and private message transmission, making [MITM attack][1] very difficult at any part of the message transmission system.

This document describes SMP protocol version 19. Versions 1-5 are discontinued. The version history:
This document describes SMP protocol version 20. Versions 1-5 are discontinued. The version history:

- v1: binary protocol encoding
- v2: message flags (used to control notifications)
Expand All @@ -103,6 +106,7 @@ This document describes SMP protocol version 19. Versions 1-5 are discontinued.
- v17: create notification credentials with NEW command
- v18: support client notices in BLOCKED error
- v19: service subscriptions to messages (SUBS, NSUBS, SOKS, ENDS, ALLS commands)
- v20: public namespaces resolver (RSLV command, NAME response) — forwarded-only via PFWD

## Introduction

Expand Down Expand Up @@ -424,6 +428,8 @@ Simplex messaging router implementations MUST NOT create, store or send to any o

- Any other information that may compromise privacy or [forward secrecy][4] of communication between clients using simplex messaging routers (the routers cannot compromise forward secrecy of any application layer protocol, such as double ratchet).

Routers with the names role make outbound JSON-RPC calls to an Ethereum endpoint to read `NameRecord` data; the lookup key reaches that endpoint. Operators MUST run the endpoint themselves (loopback Reth + Nimbus, or a self-hosted central deployment) — sharing one endpoint across multiple operators collapses the two-server privacy property because the endpoint operator would see every lookup key across all of them. The names role and the SMP-proxy role MUST NOT be enabled on the same router by default; a slow `RSLV` cache miss can serialise other forwarded commands on the same proxy-relay session.

## Message delivery notifications

Supporting message delivery while the client mobile app is not running requires sending push notifications with the device token. All alternative mechanisms for background message delivery are unreliable, particularly on iOS platform.
Expand Down Expand Up @@ -1422,6 +1428,95 @@ When the command is successfully executed by the router, it should respond with
ok = %s"OK"
```

### Resolver commands

Resolver commands implement public-namespace name resolution on the names-role
router. A names router translates an opaque lookup key (such as `alice` or
`alice.simplex.eth`) into a `NameRecord` carrying the channel and contact links
the named party publishes.

**Forwarded-only.** RSLV is only valid when delivered inside a `PFWD` block via
the SMP proxy. A direct `RSLV` from a transport client is rejected with
`ERR CMD PROHIBITED`. This preserves the two-server privacy property of the
resolver design: the names router sees the lookup key but never the client IP,
session, or identity; the proxy router sees the client connection but cannot
read the encrypted lookup key inside the forwarded transmission.

**Backing store.** This protocol does not prescribe where the names router
reads `NameRecord` from. The reference implementation queries the SNRC contract
on Ethereum via a JSON-RPC endpoint; alternative backings (different chains,
DHT, etc.) are valid as long as they return a `NameRecord` matching the encoding
below.

#### Resolve name command

The `RSLV` command carries a JSON-encoded request as the payload:

```abnf
rslv = %s"RSLV" SP json-bytes ; json-bytes consumes the remainder of the transmission
```

`json-bytes` MUST be a UTF-8 JSON object with the following schema:

| Field | JSON type | Constraints |
|---|---|---|
| `name` | string | the canonical fully-qualified name (TLD always explicit, e.g. `"privacy.simplex"`, `"test.testing"`, `"example.com"`); UTF-8 bytes only |
| `contract` | string | `"0x"` followed by 40 lowercase hex characters (20 raw bytes — the SNRC contract address the client expects the server to query) |

**Server-side validation.** The names router parses `name` as a fully-qualified
domain (TLD required — bare labels are rejected), extracts the TLD, and looks
up the expected SNRC contract address in its INI whitelist
(`registry_tld_simplex`, `registry_tld_testing`, `registry_tld_all`).
`registry_tld_all` is the catch-all used when no TLD-specific entry matches
the requested TLD (and the only entry that can resolve web domains). If no
whitelist entry matches the TLD, or if the client-supplied `contract` differs
from the configured address, the server replies with `ERR AUTH` without
contacting the chain. This lets one names router safely host multiple TLDs
(each backed by its own SNRC contract) and reject clients pointing at a
contract the operator doesn't run.

The names router responds with either a `NAME` response carrying the resolved
record, or `ERR AUTH` collapsing every failure mode (name not found, malformed
name, TLD not in whitelist, contract mismatch, names role disabled, RPC
unreachable, decode error, timeout). The wire code does not distinguish
between these — stats counters MAY be exposed out-of-band for operator
observability (`bad_name` is incremented for validation/whitelist failures,
distinct from `not_found` for valid lookups with no on-chain record).

#### Name record response

The `NAME` response carries a JSON-encoded record as the payload:

```abnf
name = %s"NAME" SP json-bytes ; json-bytes consumes the remainder of the transmission
```

`json-bytes` MUST be a UTF-8 JSON object with the following schema:

| Field | JSON type | Constraints |
|---|---|---|
| `displayName` | string | ≤ 255 bytes UTF-8 |
| `owner` | string | `"0x"` followed by 40 lowercase hex characters (20 raw bytes) |
| `channelLinks` | array of strings | each ≤ 1024 bytes UTF-8; combined count of `channelLinks + contactLinks` ≤ 8 |
| `contactLinks` | array of strings | each ≤ 1024 bytes UTF-8; combined count cap shared with `channelLinks` |
| `adminAddress` | string or null | ≤ 255 bytes UTF-8; senders MUST emit `null` when unset; receivers MUST also accept absent keys as unset |
| `adminEmail` | string or null | ≤ 255 bytes UTF-8; senders MUST emit `null` when unset; receivers MUST also accept absent keys as unset |
| `expiry` | integer | Int64 Unix seconds, MUST be ≥ 0; `0` means "never expires" |
| `isTest` | boolean | true on testnet deployments |

Receivers MUST tolerate extra unknown fields (forward-compatibility for future
field additions). Adding a required field is a breaking change requiring an
SMP version bump.

**Canonical encoding.** Two names routers reading the same backing state and
producing the same `NameRecord` MUST emit byte-identical JSON: emit object
keys in the order listed above, integers without decimal points, no
insignificant whitespace.

**Wire-size budget.** A maximal `nameRecord` (8 × 1024-byte links plus
maximal admin / display strings) JSON-encodes to roughly 9 KB, well under the
SMP proxied transmission budget of 16224 bytes.

## Transport connection with the SMP router

### General transport protocol considerations
Expand Down
8 changes: 8 additions & 0 deletions simplexmq.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ library
Simplex.Messaging.Server.QueueStore.Postgres.Config
Simplex.Messaging.Server.QueueStore.QueueInfo
Simplex.Messaging.ServiceScheme
Simplex.Messaging.SimplexName
Simplex.Messaging.Session
Simplex.Messaging.SystemTime
Simplex.Messaging.TMap
Expand Down Expand Up @@ -261,6 +262,9 @@ library
Simplex.Messaging.Server.MsgStore.Journal.SharedLock
Simplex.Messaging.Server.MsgStore.STM
Simplex.Messaging.Server.MsgStore.Types
Simplex.Messaging.Server.Names
Simplex.Messaging.Server.Names.Eth.RPC
Simplex.Messaging.Server.Names.Eth.SNRC
Simplex.Messaging.Server.NtfStore
Simplex.Messaging.Server.Prometheus
Simplex.Messaging.Server.QueueStore
Expand Down Expand Up @@ -355,7 +359,10 @@ library
build-depends:
case-insensitive ==1.2.*
, hashable ==1.4.*
, http-client >=0.7 && <0.8
, http-client-tls >=0.3 && <0.4
, ini ==0.4.1
, network-uri >=2.6 && <2.7
, optparse-applicative >=0.15 && <0.17
, process ==1.6.*
, temporary ==1.3.*
Expand Down Expand Up @@ -508,6 +515,7 @@ test-suite simplexmq-test
ServerTests
SMPAgentClient
SMPClient
SMPNamesTests
SMPProxyTests
Util
XFTPAgent
Expand Down
62 changes: 2 additions & 60 deletions src/Simplex/Messaging/Agent/Protocol.hs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ module Simplex.Messaging.Agent.Protocol
ConnectionLink (..),
AConnectionLink (..),
SimplexNameInfo (..),
SimplexNameDomain (..),
SimplexTLD (..),
SimplexNameType (..),
ConnShortLink (..),
Expand Down Expand Up @@ -236,6 +237,7 @@ import Simplex.Messaging.Crypto.Ratchet
)
import Simplex.Messaging.Encoding
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.SimplexName (SimplexNameDomain (..), SimplexNameInfo (..), SimplexNameType (..), SimplexTLD (..), fullDomainName, shortNameInfoStr)
import Simplex.Messaging.Parsers
import Simplex.Messaging.Protocol
( AProtocolType,
Expand Down Expand Up @@ -1530,61 +1532,6 @@ instance (Typeable c, ConnectionModeI c) => FromField (ConnShortLink c) where fr

data ContactConnType = CCTContact | CCTChannel | CCTGroup | CCTRelay deriving (Eq, Show)

data SimplexNameInfo = SimplexNameInfo
{ nameType :: SimplexNameType,
nameTLD :: SimplexTLD,
domain :: Text,
subDomain :: [Text] -- parent to child: ["b", "a"] for a.b.domain.simplex
}
deriving (Eq, Show)

data SimplexTLD = TLDSimplex | TLDTesting | TLDWeb
deriving (Eq, Show)

data SimplexNameType = NTPublicGroup | NTContact
deriving (Eq, Show)

instance StrEncoding SimplexNameType where
strEncode = \case
NTPublicGroup -> "#"
NTContact -> "@"
strP = A.char '#' $> NTPublicGroup <|> A.char '@' $> NTContact

instance StrEncoding SimplexNameInfo where
strEncode info = "simplex:/name" <> strEncode (nameType info) <> encodeUtf8 (fullDomainName info)
strP = optional "simplex:/name" *> (strP >>= nameP) <|> nameP NTPublicGroup
where
nameP nt = parseName nt . safeDecodeUtf8 <$?> A.takeWhile1 (not . A.isSpace)
parseName nt s = AT.parseOnly (nameLabelP `AT.sepBy1` AT.char '.' <* AT.endOfInput) s >>= mkNameInfo nt
nameLabelP = T.intercalate "-" <$> AT.takeWhile1 (\c -> isNameLetter c || isDigit c) `AT.sepBy1` AT.char '-'
isNameLetter c = isAlpha c && not (c >= '\x00c0' && c <= '\x024f')
mkNameInfo nt labels = case reverse labels of
[] -> Left "empty name"
[name]
| nt == NTPublicGroup -> Right $ SimplexNameInfo nt TLDSimplex name []
| otherwise -> Left "contact name requires TLD"
tld : name : sub -> Right $ case tld of
"simplex" -> SimplexNameInfo nt TLDSimplex name sub
"testing" -> SimplexNameInfo nt TLDTesting name sub
_ -> SimplexNameInfo nt TLDWeb (T.intercalate "." labels) []

fullDomainName :: SimplexNameInfo -> Text
fullDomainName SimplexNameInfo {nameTLD, domain, subDomain} = T.intercalate "." (reverse subDomain ++ [domain] ++ tld')
where
tld' = case nameTLD of
TLDSimplex -> ["simplex"]
TLDTesting -> ["testing"]
TLDWeb -> []

shortNameInfoStr :: SimplexNameInfo -> Text
shortNameInfoStr = \case
SimplexNameInfo {nameType = NTPublicGroup, nameTLD = TLDSimplex, domain, subDomain = []} -> "#" <> domain
info -> pfx <> fullDomainName info
where
pfx = case nameType info of
NTPublicGroup -> "#"
NTContact -> "@"

data AConnShortLink = forall m. ConnectionModeI m => ACSL (SConnectionMode m) (ConnShortLink m)

instance Eq AConnShortLink where
Expand Down Expand Up @@ -2263,8 +2210,3 @@ instance ToJSON ACreatedConnLink where
toEncoding (ACCL _ ccLink) = toEncoding ccLink
toJSON (ACCL _ ccLink) = toJSON ccLink

$(J.deriveJSON (enumJSON $ dropPrefix "TLD") ''SimplexTLD)

$(J.deriveJSON (enumJSON $ dropPrefix "NT") ''SimplexNameType)

$(J.deriveJSON defaultJSON ''SimplexNameInfo)
1 change: 1 addition & 0 deletions src/Simplex/Messaging/Encoding.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module Simplex.Messaging.Encoding
smpEncodeList,
smpListP,
lenEncode,
lenP,
)
where

Expand Down
Loading
Loading