Any Naming System node
Please see Any Naming System repository for rationale and more info.
This global singleton node provides access to AnyNS smart contracts. You can call smart contracts either directly or by using this dRPC service.
Building and Running
- To build:
make deps build
- To run:
go run ./cmd --c=NODE_CONFIG
- To run as a client:
go run ./cmd --c=CLIENT_CONFIG --cl --cmd=COMMAND --params=PARAMS_JSON
Available client commands
1. is-name-available
Check if name is available. If not - it will return information
Parameters: '{ "FullName": "xxx.any"}'.
Example: go run ./cmd --c=config-client.yml --cl --cmd=is-name-available --params='{ "FullName": "xxx.any"}'
2. name-register
Create an operation to register a new name.
Parameters: '{ "FullName": "suppa.any", "OwnerAnyAddress": "A6WVkd1MxX1i7hGQCcDhMFvfEzokPppRzxve2wdhTZ8jZTio", "OwnerEthAddress": "0xe595e2BA3f0cE990d8037e07250c5C78ce40f8fF", "SpaceId": "bafybeibs62gqtignuckfqlcr7lhhihgzh2vorxtmc5afm6uxh4zdcmuwuu"}'.
.yml config files
Please see example in the 'etc' subfolder.
NOTICE: in order to call methods as an Admin - account.signKey should be used to sign messages.
That's why you can run N ns nodes with different account.peerId and account.peerKey but with SAME account.signKey.
Contracts section
contracts:
// use your own geth node or Infura/Alchemy/Moralis/etc API
gethUrl: https://sepolia.infura.io/v3/XXX
// https://github.com/anyproto/any-ns/blob/master/deployments/sepolia/ENSRegistry.json
ensRegistry: 0xc0D3c96aE923Da6b45E6d4c21a0424730a20BCA9
// https://github.com/anyproto/any-ns/blob/master/deployments/sepolia/AnytypeResolver.json
resolver: 0x34F9c5CB9b6dcc036e045a15af20CEdC0dE4dcB2
// https://github.com/anyproto/any-ns/blob/master/deployments/sepolia/AnytypeRegistrarImplementation.json
registrarController: 0x6BA138bb7B1Bdea2B127D55D7C8F0DC9467b424E
// https://github.com/anyproto/any-ns/blob/master/deployments/sepolia/AnytypeRegistrarControllerPrivate.json
registrarControllerPrivate: 0x45bA047AD44e35FbF5A1375F79ea3872ceDB1732
// https://github.com/anyproto/any-ns/blob/master/deployments/sepolia/AnytypeNameWrapper.json
nameWrapper: 0xFe69BF9B3fD69d09977b37b5953C8B43687f3B23
// Admin address
admin: 0x61d1eeE7FBF652482DEa98A1Df591C626bA09a60
// Admin key
adminPk: XXX
Name cache (cache section)
The Mongo cache collection mirrors the contracts; with readFromCache: true the lookups are served
from it. The rule: a name is reported as available, and a cached record is dropped, only when the
chain confirmed it. When unsure, the cached record is served as taken and refreshed in the background.
- Reads. Every read and the cache key use the canonical spelling of the name (the
normalization of the registration,
ensip15validation). A registry owner without a registrar
expiry is a failure (retried), never a lapse; a name without a registry owner that the
registrar still reserves (reclaimed to address(0), unexpired) stays taken. A refresh reads the latest block header once (number, hash and timestamp as the node
reports them) and pins every contract read to that block by its hash. A name is registered if the
registry has an owner and nameExpires + 90 days (grace) >= block.timestamp.
- Writes. One record per name (the unique index on
{name: 1}; the node accepts an existing one
whatever its name, e.g. name_1, creates it if it is missing, refuses to start if it is not
unique). Every write is one Mongo transaction (majority, primary, bounded) that replaces the
record only with a newer observation: the higher block wins. The node refuses to start without a
replica set unless allowUnsafeStandalone is set (local development only).
- Removal. A name becomes available only when the latest block says it is not registered and
a re-read at the finalized (fallback: safe) block confirms it. Only the background refresh and the
backfill read the finalized block; a request (
readFromCache: false, GetOperation) keeps the
record and hands the name to the background. The record then becomes a
tombstone at the finalized block: lookups treat it as a cache miss, reverse lookups skip it, and
an older observation can not replace it. Otherwise the record stays as it is.
- Changes (a new registration, another owner, a renewal) are read again at +5 and +30
minutes, whoever wrote them. A read whose enrichment failed keeps the owner fields of the
previous record only where it read the same wallet (the EOA owner of that wallet); otherwise
they stay empty until the retry. It never replaces a complete record of the same block. Every
record stores its canonical spelling (
canon).
- Lookups never read the contracts and never write. A record that can be stale (expired,
lapsed, incomplete, an old tombstone) is served as it is and handed to a background worker through
a non-blocking in-memory queue (dropped when full).
- Background refresh. The worker refreshes under a lease in Mongo shared by the ns nodes; a
failure backs off. Every record that needs a refresh is due in Mongo (
repair_at, indexed):
incomplete records, registrations when they expire, expired ones when they lapse and once a day,
and scheduled re-reads. A periodic scan (repairIntervalSec) takes a bounded batch, the longest
waiting first.
- GetOperation answers as before (GO-7482). A completed operation's name is refreshed at the
latest block: in the poll if it is not cached yet (as before), in the background if it is
(the poll waits neither for the contracts nor for a write: the re-reads are stored on the record
in the background, bounded by 2 seconds, coalesced and capped, so a dropped refresh request or a
restart after that write can not lose them). Re-reads are scheduled at +5 and +30 minutes (lagging providers,
reorgs past the Sepolia finality). If the background refresh fails, the record is marked
refresh_needed; its data stays.
cache:
// the cache needs a replica set (transactions). the node refuses to start on a standalone Mongo
// unless this is set: local development ONLY, concurrent writes are not ordered there
allowUnsafeStandalone: false
// seconds between the periodic scans of the background refresh. 0: 60, negative: off
repairIntervalSec: 0
Maintenance (one-off runs of the node binary; all are dry runs unless -refresh-apply is set):
-dedupe-cache: verifies the unique index on {name: 1}. An existing unique one is accepted
(unique name index=exists). A missing one is created (with -refresh-apply). A
non-unique one, or duplicates of a name, is an error (exit 1); the index is never dropped.
It then migrates records cached under a non-canonical spelling (e.g. Foo.any, written by an
old node), one transaction each: no canonical record → renamed to the canonical name (data
kept, marked for a refresh); a live, complete canonical record at least as new → the alias is
deleted (only if the canonical record has its chain block: a legacy record without one has
unknown freshness); otherwise both stay, the canonical one is marked for a refresh, and they are
listed (kept:) for an operator or a later run. Until then the node warns at start, a live alias
keeps its name taken (every lookup without a live canonical record checks for one, through a
case- and accent-insensitive index name_ci the node creates; the candidates are checked by the normalization), reverse lookups prefer the canonical record,
and the background refreshes the canonical name instead of the alias.
-dedupe-cache -refresh-apply also gives every record its canon: the alias check finds an
alias by it (e.g. a punycode spelling, which the collation can not equate).
Aliases are never deleted automatically except by the rule above (a canonical record read from
the chain, live, complete and at least as new). Every other alias stays and keeps its name
taken (the safe direction): -dedupe-cache lists them (kept:), -refresh-cache counts
them (non-canonical=N), the node warns at start. An operator resolves them by hand: check the
name on chain (e.g. -refresh-cache for the canonical name), then delete the alias record
(db.cache.deleteOne({name: "<alias spelling>"})) if the canonical record is right.
-refresh-cache: re-reads every cached name (tombstones too) with the same decisions as the node.
-refresh-interval is the delay between two names (default 1s). Exits 1 if any name failed,
including names whose finalized block could not be read. not-final (the finalized block does not
confirm a removal yet) is counted separately and is not a failure: run it again later.
non-canonical counts records cached under a spelling other than the canonical one (e.g.
Foo.any): the canonical name is refreshed, such a record is left as it is (taken) for an
operator to look at.
-purge-tombstones: deletes the tombstones before a rollback (see below). It refuses (exit 1) while
the cache has incomplete records without an owner; it lists them.
Rollout (both ns nodes share the cache):
- Mongo must be a replica set (prod:
rs0).
anynsnode -c <config> -dedupe-cache (prod: expect unique name index=exists).
- Upgrade both ns nodes together. In the short mixed window an old node still serves a lapsed
name as taken (today's behaviour) and still writes records without the new fields.
- Only after both run the new version:
anynsnode -c <config> -dedupe-cache (dry run), then with
-refresh-apply (migrates the non-canonical records, including any written in the mixed window).
- Then
anynsnode -c <config> -refresh-cache (dry run), then with
-refresh-apply. It removes lapsed names (confirmed at a finalized block), fixes stale records
(including what an old node wrote in the mixed window) and schedules every record for the
periodic scan. Re-run until failed=0; not-final names are picked up by the background.
Rollback to a version older than GO-7567. Old versions read ANY record of a name as a taken name, a
tombstone too, and never repair a record without an owner:
anynsnode -c <config> -purge-tombstones (dry run), then with -refresh-apply. If it lists
incomplete records, run -refresh-cache -refresh-apply first.
- Deploy the old binaries on both nodes.
- The new nodes can write tombstones until they stop: run step 1 once more (with the new binary, a
one-off process) after the last new node stopped.
Old versions ignore the new fields and the repair_at index (they can be dropped later); the unique
index on {name: 1} stays (old versions upsert by name).
Not in this version (follow-ups): pinning the post-operation read to the operation receipt's block
(and the archive-state dependency that brings), a per-operation "cache synced" flag, verifying a
changed record against the finalized state (replaced here by the scheduled re-reads), removal
watermarks / a delete mode instead of tombstones, lookup-triggered Mongo writes, a CI job with
Mongo fail points.
Tests of the cache need a Mongo: a standalone one on localhost:27017 (the replica set tests skip),
or a one-node replica set. The fail point tests (stalled Mongo commands) need enableTestCommands:
docker run -d --name any-ns-test-rs -p 27018:27018 mongo:7 --replSet rs0 --port 27018 --bind_ip_all \
--setParameter enableTestCommands=1
mongosh --port 27018 --eval 'rs.initiate({_id:"rs0",members:[{_id:0,host:"localhost:27018"}]})'
ANY_NS_TEST_MONGO="mongodb://localhost:27018/?replicaSet=rs0" go test -p 1 ./cache/ ./anynsrpc/ ./anynsaarpc/
Contribution
Thank you for your desire to develop Anytype together!
❤️ This project and everyone involved in it is governed by the Code of Conduct.
🧑💻 Check out our contributing guide to learn about asking questions, creating issues, or submitting pull requests.
🫢 For security findings, please email security@anytype.io and refer to our security guide for more information.
🤝 Follow us on Github and join the Contributors Community.
Made by Any — a Swiss association 🇨🇭
Licensed under MIT.