Skip to content

Commit 5ee014d

Browse files
committed
namespace: reshape NameRecord JSON to align with Python resolver
Convergence on the Python resolver shape (PR #1795 `snrc-resolve.py`) so a names router can be backed either by the direct-ETH-RPC resolver or by the Python REST resolver without changing the wire format clients see. Wire-level changes: - Add `nickname`, `website`, `location`, `simplex.contact`, `simplex.channel`, `ETH`, `BTC`, `XMR`, `DOT`, `resolver` (SNRC contract address that produced the record); all but `name`, `owner`, `resolver` are optional. - Drop `displayName` (now `name`), `channelLinks`, `contactLinks`, `adminAddress`, `adminEmail`, `expiry`, `isTest`. - The wire NameRecord no longer carries `expiry`; clients trust the server's filter. Expiry checking moves into `decodeGetRecord`, which now takes a `nowSec :: Int64` argument (the placeholder remains, but the field-layout-aware decoder will apply the filter once it lands). - Testnet status is derived from the queried TLD (`TLDTesting` vs `TLDSimplex`) rather than an in-record flag. Other: - ToJSON/FromJSON are hand-rolled because Aeson TH doesn't accommodate dot-keys (`simplex.contact`) or uppercase coin keys (`ETH`/`BTC`...). - `NameLink` newtype is removed (no longer used internally); per-field byte caps are applied directly in the FromJSON parser. - Update the canonical-encoding spec in protocol/simplex-messaging.md.
1 parent ecd89cf commit 5ee014d

5 files changed

Lines changed: 152 additions & 137 deletions

File tree

protocol/simplex-messaging.md

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,14 +1493,23 @@ name = %s"NAME" SP json-bytes ; json-bytes consumes the remainder of the trans
14931493

14941494
| Field | JSON type | Constraints |
14951495
|---|---|---|
1496-
| `displayName` | string | ≤ 255 bytes UTF-8 |
1496+
| `name` | string | ≤ 255 bytes UTF-8 |
1497+
| `nickname` | string or null | ≤ 255 bytes UTF-8; senders MUST emit `null` when unset; receivers MUST also accept absent keys as unset |
1498+
| `website` | string or null | ≤ 255 bytes UTF-8; same null / absent rules |
1499+
| `location` | string or null | ≤ 255 bytes UTF-8; same null / absent rules |
1500+
| `simplex.contact` | string or null | ≤ 1024 bytes UTF-8; same null / absent rules |
1501+
| `simplex.channel` | string or null | ≤ 1024 bytes UTF-8; same null / absent rules |
1502+
| `ETH` | string or null | ≤ 255 bytes UTF-8; same null / absent rules |
1503+
| `BTC` | string or null | ≤ 255 bytes UTF-8; same null / absent rules |
1504+
| `XMR` | string or null | ≤ 255 bytes UTF-8; same null / absent rules |
1505+
| `DOT` | string or null | ≤ 255 bytes UTF-8; same null / absent rules |
14971506
| `owner` | string | `"0x"` followed by 40 lowercase hex characters (20 raw bytes) |
1498-
| `channelLinks` | array of strings | each ≤ 1024 bytes UTF-8; combined count of `channelLinks + contactLinks` ≤ 8 |
1499-
| `contactLinks` | array of strings | each ≤ 1024 bytes UTF-8; combined count cap shared with `channelLinks` |
1500-
| `adminAddress` | string or null | ≤ 255 bytes UTF-8; senders MUST emit `null` when unset; receivers MUST also accept absent keys as unset |
1501-
| `adminEmail` | string or null | ≤ 255 bytes UTF-8; senders MUST emit `null` when unset; receivers MUST also accept absent keys as unset |
1502-
| `expiry` | integer | Int64 Unix seconds, MUST be ≥ 0; `0` means "never expires" |
1503-
| `isTest` | boolean | true on testnet deployments |
1507+
| `resolver` | string | `"0x"` followed by 40 lowercase hex characters; the SNRC contract address that produced the record |
1508+
1509+
The server MUST filter expired records before constructing the response
1510+
(returning `ERR AUTH` to the client), so the wire format carries no expiry
1511+
field. Testnet-vs-mainnet status is derived from the queried TLD rather than
1512+
an in-record flag.
15041513

15051514
Receivers MUST tolerate extra unknown fields (forward-compatibility for future
15061515
field additions). Adding a required field is a breaking change requiring an
@@ -1511,8 +1520,8 @@ producing the same `NameRecord` MUST emit byte-identical JSON: emit object
15111520
keys in the order listed above, integers without decimal points, no
15121521
insignificant whitespace.
15131522

1514-
**Wire-size budget.** A maximal `nameRecord` (8 × 1024-byte links plus
1515-
maximal admin / display strings) JSON-encodes to roughly 9 KB, well under the
1523+
**Wire-size budget.** A maximal `nameRecord` (two 1024-byte SimpleX links
1524+
plus the other capped strings) JSON-encodes to roughly 4 KB, well under the
15161525
SMP proxied transmission budget of 16224 bytes.
15171526

15181527
## Transport connection with the SMP router

src/Simplex/Messaging/Protocol.hs

Lines changed: 53 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,6 @@ module Simplex.Messaging.Protocol
168168
NameOwner,
169169
mkNameOwner,
170170
unNameOwner,
171-
NameLink,
172-
mkNameLink,
173-
unNameLink,
174171
MsgFlags (..),
175172
initialSMPClientVersion,
176173
currentSMPClientVersion,
@@ -774,80 +771,79 @@ instance J.FromJSON RslvRequest where
774771
contract <- o J..: "contract"
775772
pure RslvRequest {name, contract}
776773

777-
-- | A name-record link (channel or contact). Bare constructor not exported;
778-
-- use `mkNameLink` to enforce the ≤1024-byte UTF-8 invariant.
779-
newtype NameLink = NameLink Text
780-
deriving (Eq, Show)
781-
782-
mkNameLink :: Text -> Either String NameLink
783-
mkNameLink t
784-
| B.length (encodeUtf8 t) <= 1024 = Right (NameLink t)
785-
| otherwise = Left "NameLink too long"
786-
787-
unNameLink :: NameLink -> Text
788-
unNameLink (NameLink t) = t
789-
{-# INLINE unNameLink #-}
790-
791-
instance J.ToJSON NameLink where
792-
toJSON (NameLink t) = J.toJSON t
793-
794-
instance J.FromJSON NameLink where
795-
parseJSON = J.withText "NameLink" (either fail pure . mkNameLink)
796-
797774
-- | Resolved name record returned by the names role.
798775
-- Wire format is JSON — change requires an SMP version bump.
776+
-- JSON keys match the Python resolver (PR #1795 `snrc-resolve.py`) so the
777+
-- same server can be backed by either the direct-ETH-RPC resolver or the
778+
-- Python REST resolver without changing the wire format clients see.
799779
data NameRecord = NameRecord
800-
{ nrDisplayName :: Text,
780+
{ nrName :: Text,
781+
nrNickname :: Maybe Text,
782+
nrWebsite :: Maybe Text,
783+
nrLocation :: Maybe Text,
784+
nrSimplexContact :: Maybe Text,
785+
nrSimplexChannel :: Maybe Text,
786+
nrEth :: Maybe Text,
787+
nrBtc :: Maybe Text,
788+
nrXmr :: Maybe Text,
789+
nrDot :: Maybe Text,
801790
nrOwner :: NameOwner,
802-
nrChannelLinks :: [NameLink],
803-
nrContactLinks :: [NameLink],
804-
nrAdminAddress :: Maybe Text,
805-
nrAdminEmail :: Maybe Text,
806-
nrExpiry :: Int64, -- Unix seconds, ≥ 0
807-
nrIsTest :: Bool
791+
nrResolver :: NameOwner -- SNRC contract address that produced the record
808792
}
809793
deriving (Eq, Show)
810794

795+
-- Hand-rolled JSON instances: dot-keys ("simplex.contact", "simplex.channel")
796+
-- and uppercase coin keys ("ETH", "BTC", "XMR", "DOT") fall outside Aeson TH's
797+
-- field-label conventions.
811798
instance J.ToJSON NameRecord where
812-
toJSON NameRecord {nrDisplayName, nrOwner, nrChannelLinks, nrContactLinks, nrAdminAddress, nrAdminEmail, nrExpiry, nrIsTest} =
799+
toJSON NameRecord {nrName, nrNickname, nrWebsite, nrLocation, nrSimplexContact, nrSimplexChannel, nrEth, nrBtc, nrXmr, nrDot, nrOwner, nrResolver} =
813800
J.object
814-
[ "displayName" J..= nrDisplayName,
801+
[ "name" J..= nrName,
802+
"nickname" J..= nrNickname,
803+
"website" J..= nrWebsite,
804+
"location" J..= nrLocation,
805+
"simplex.contact" J..= nrSimplexContact,
806+
"simplex.channel" J..= nrSimplexChannel,
807+
"ETH" J..= nrEth,
808+
"BTC" J..= nrBtc,
809+
"XMR" J..= nrXmr,
810+
"DOT" J..= nrDot,
815811
"owner" J..= nrOwner,
816-
"channelLinks" J..= nrChannelLinks,
817-
"contactLinks" J..= nrContactLinks,
818-
"adminAddress" J..= nrAdminAddress,
819-
"adminEmail" J..= nrAdminEmail,
820-
"expiry" J..= nrExpiry,
821-
"isTest" J..= nrIsTest
812+
"resolver" J..= nrResolver
822813
]
823814
-- explicit toEncoding to preserve the spec-documented key order; the default
824815
-- routes through Value/KeyMap and re-emits keys alphabetically, breaking the
825816
-- "two routers MUST emit byte-identical JSON" requirement.
826-
toEncoding NameRecord {nrDisplayName, nrOwner, nrChannelLinks, nrContactLinks, nrAdminAddress, nrAdminEmail, nrExpiry, nrIsTest} =
817+
toEncoding NameRecord {nrName, nrNickname, nrWebsite, nrLocation, nrSimplexContact, nrSimplexChannel, nrEth, nrBtc, nrXmr, nrDot, nrOwner, nrResolver} =
827818
J.pairs $
828-
"displayName" J..= nrDisplayName
819+
"name" J..= nrName
820+
<> "nickname" J..= nrNickname
821+
<> "website" J..= nrWebsite
822+
<> "location" J..= nrLocation
823+
<> "simplex.contact" J..= nrSimplexContact
824+
<> "simplex.channel" J..= nrSimplexChannel
825+
<> "ETH" J..= nrEth
826+
<> "BTC" J..= nrBtc
827+
<> "XMR" J..= nrXmr
828+
<> "DOT" J..= nrDot
829829
<> "owner" J..= nrOwner
830-
<> "channelLinks" J..= nrChannelLinks
831-
<> "contactLinks" J..= nrContactLinks
832-
<> "adminAddress" J..= nrAdminAddress
833-
<> "adminEmail" J..= nrAdminEmail
834-
<> "expiry" J..= nrExpiry
835-
<> "isTest" J..= nrIsTest
830+
<> "resolver" J..= nrResolver
836831

837832
instance J.FromJSON NameRecord where
838833
parseJSON = J.withObject "NameRecord" $ \o -> do
839-
nrDisplayName <- o J..: "displayName" >>= capUtf8 "displayName" 255
834+
nrName <- o J..: "name" >>= capUtf8 "name" 255
835+
nrNickname <- o J..:? "nickname" >>= traverse (capUtf8 "nickname" 255)
836+
nrWebsite <- o J..:? "website" >>= traverse (capUtf8 "website" 255)
837+
nrLocation <- o J..:? "location" >>= traverse (capUtf8 "location" 255)
838+
nrSimplexContact <- o J..:? "simplex.contact" >>= traverse (capUtf8 "simplex.contact" 1024)
839+
nrSimplexChannel <- o J..:? "simplex.channel" >>= traverse (capUtf8 "simplex.channel" 1024)
840+
nrEth <- o J..:? "ETH" >>= traverse (capUtf8 "ETH" 255)
841+
nrBtc <- o J..:? "BTC" >>= traverse (capUtf8 "BTC" 255)
842+
nrXmr <- o J..:? "XMR" >>= traverse (capUtf8 "XMR" 255)
843+
nrDot <- o J..:? "DOT" >>= traverse (capUtf8 "DOT" 255)
840844
nrOwner <- o J..: "owner"
841-
nrChannelLinks <- o J..: "channelLinks"
842-
nrContactLinks <- o J..: "contactLinks"
843-
when (length nrChannelLinks + length nrContactLinks > 8) $
844-
fail "combined channelLinks + contactLinks > 8"
845-
nrAdminAddress <- o J..:? "adminAddress" >>= traverse (capUtf8 "adminAddress" 255)
846-
nrAdminEmail <- o J..:? "adminEmail" >>= traverse (capUtf8 "adminEmail" 255)
847-
nrExpiry <- o J..: "expiry"
848-
when (nrExpiry < 0) $ fail "expiry must be non-negative"
849-
nrIsTest <- o J..: "isTest"
850-
pure NameRecord {nrDisplayName, nrOwner, nrChannelLinks, nrContactLinks, nrAdminAddress, nrAdminEmail, nrExpiry, nrIsTest}
845+
nrResolver <- o J..: "resolver"
846+
pure NameRecord {nrName, nrNickname, nrWebsite, nrLocation, nrSimplexContact, nrSimplexChannel, nrEth, nrBtc, nrXmr, nrDot, nrOwner, nrResolver}
851847
where
852848
capUtf8 fld lim t
853849
| B.length (encodeUtf8 t) <= lim = pure t

src/Simplex/Messaging/Server/Names.hs

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import Data.Text.Encoding (encodeUtf8)
4141
import Data.Time.Clock.POSIX (getPOSIXTime)
4242
import Simplex.Messaging.Encoding.String (strDecode)
4343
import Simplex.Messaging.Util (eitherToMaybe)
44-
import Simplex.Messaging.Protocol (NameOwner, NameRecord (..), RslvRequest (..), unNameOwner)
44+
import Simplex.Messaging.Protocol (NameOwner, NameRecord, RslvRequest (..), unNameOwner)
4545
import Simplex.Messaging.Server.Names.Eth.RPC (EthRpcEnv, EthRpcError (..), RpcAuth (..), closeEthRpcEnv, ethCallReal, newEthRpcEnv)
4646
import Simplex.Messaging.Server.Names.Eth.SNRC (decodeAddress, decodeGetRecord, encodeGetRecord, isZeroOwner, namehash)
4747
import Simplex.Messaging.SimplexName (SimplexNameDomain (..), SimplexTLD (..), fullDomainName)
@@ -166,34 +166,27 @@ resolveName env contract d = do
166166
pure (Left EthHttpErr)
167167

168168
fetch :: NamesEnv -> NameOwner -> SimplexNameDomain -> IO (Either ResolveError NameRecord)
169-
fetch env@NamesEnv {ethCall} contract d =
169+
fetch env@NamesEnv {ethCall} contract d = do
170+
nowSec <- floor <$> getPOSIXTime
170171
ethCall (unNameOwner contract) (encodeGetRecord (namehash (encodeUtf8 (fullDomainName d)))) >>= \case
171172
Left e -> pure (Left (mapEthRpcError e))
172-
Right ret -> case decodeGetRecord ret of
173+
Right ret -> case decodeGetRecord nowSec ret of
173174
Right Nothing -> notFoundWithPlaceholderWarn ret
174-
Right (Just rec) -> checkExpiry rec
175+
Right (Just rec) -> pure (Right rec)
175176
Left _ -> pure (Left EthDecodeErr)
176177
where
177178
-- decodeGetRecord is currently a placeholder: it returns Right Nothing
178179
-- for BOTH "zero-owner sentinel" (real NotFound) and "non-zero owner
179180
-- with real data but no ABI decoder yet". Inspect the owner slot
180181
-- directly to distinguish, and surface the latter once per process so
181182
-- an operator who enables [NAMES] against a working SNRC contract sees
182-
-- the resolver is functionally stubbed.
183+
-- the resolver is functionally stubbed. Expired records are filtered
184+
-- inside the decoder (using the `nowSec` argument) so the wire
185+
-- NameRecord never carries an expiry field.
183186
notFoundWithPlaceholderWarn ret = do
184187
forM_ (eitherToMaybe (decodeAddress 32 ret)) $ \owner ->
185188
unless (isZeroOwner owner) (warnPlaceholderOnce env)
186189
pure (Left NotFound)
187-
-- Defense in depth: the SNRC contract should already return the
188-
-- zero-owner sentinel for expired records, but a buggy / pre-upgrade
189-
-- contract might not. nrExpiry == 0 means "never expires" (reserved
190-
-- names); any positive expiry in the past is treated as NotFound.
191-
checkExpiry rec = do
192-
nowSec <- floor <$> getPOSIXTime
193-
pure $
194-
if nrExpiry rec /= 0 && nrExpiry rec < nowSec
195-
then Left NotFound
196-
else Right rec
197190

198191
warnPlaceholderOnce :: NamesEnv -> IO ()
199192
warnPlaceholderOnce NamesEnv {placeholderWarned} = do

src/Simplex/Messaging/Server/Names/Eth/SNRC.hs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,17 +170,24 @@ decodeStringArray depth headEnd off cntCap byteCap buf
170170

171171
-- | Decode the ABI-encoded return value of getRecord(bytes32) into a NameRecord.
172172
-- Zero-owner (0x000...000) is reported as Right Nothing so the caller maps it
173-
-- to NotFound (ENS-style sentinel).
173+
-- to NotFound (ENS-style sentinel). Records whose on-chain expiry is in the
174+
-- past are also reported as Right Nothing — clients trust the server's filter
175+
-- and the wire NameRecord carries no expiry field.
176+
--
177+
-- `nowSec` is the current Unix time the caller wants the expiry compared
178+
-- against. Pass `0` to disable the expiry check.
174179
--
175180
-- PLACEHOLDER: returns Right Nothing for any non-zero owner until the Part 1
176181
-- SNRC contract ABI is finalised. All ABI primitives above are production-ready;
177-
-- only the field-layout-aware composition is pending.
178-
decodeGetRecord :: ByteString -> Either AbiError (Maybe NameRecord)
179-
decodeGetRecord buf
182+
-- only the field-layout-aware composition (and the expiry slot read) is
183+
-- pending.
184+
decodeGetRecord :: Int64 -> ByteString -> Either AbiError (Maybe NameRecord)
185+
decodeGetRecord _nowSec buf
180186
| B.length buf < 32 * 8 = Left AbiTruncated
181187
-- Both arms return Nothing today: the zero-owner branch is the real ENS-style
182-
-- NotFound sentinel; the non-zero branch is the SNRC-ABI placeholder. They
183-
-- separate once the field-layout decoder lands.
188+
-- NotFound sentinel; the non-zero branch is the SNRC-ABI placeholder (which
189+
-- will also apply the `_nowSec` expiry filter once the field layout lands).
190+
-- They separate once the field-layout decoder ships.
184191
| otherwise = Nothing <$ decodeAddress 32 buf
185192

186193
isZeroOwner :: NameOwner -> Bool

0 commit comments

Comments
 (0)