A TLD owner needed to run their namespace without the operator: set what every name under the TLD sells for, take the TLD off the public registry for a while, and still register names under it themselves. Both settings live on chain in the TLD's TUPD records (`price`, `hidden`) because changes are rare and every client already walks the TLD beacon. The gateway's /api/tlds now carries records, owner, price_usd and hidden_by per TLD; pricing.js quotes the owner price ahead of the length tiers and only trusts a TLD_BEACON-sourced list; the register flow refuses hidden and frozen TLDs for the public but lets the owner through at the platform share only (the 90% owner cut would be paid to themselves). The portal's TLDs tab loads real holdings, shows price and on/off state, and gives each TLD a one-click switch, a price/policy editor and an inline "register a name under .tld" form. Docs and the design table describe the two new records.
6.4 KiB
Signed records manifest (BNS records v1)
Status: shipped v1 — gateway GET/POST /api/records/<name> and
verified GET /api/dns/<name> live on silentmode.st/navigate.st;
owner-tracking in the indexer live; portal DNS editor + wallet-side
signMessage + signRecordsManifest shipped. Native DNS server
(UDP :53 → BCDN name → verified records) is a v2 follow-up.
Related: DESIGN-tld-registry.md, PROTOCOL.md, public explainer at
silentmode.st/sirius-x/docs/#signed-records. Mirrored to
site-sirius-x/docs/DESIGN-signed-records-manifest.md so it rides the
next push-split to the sirius forge.
Problem
Every records edit today is a chain UPD:
- costs ~1,300 sat + service fee
- caps records at ~150 bytes of payload
- rules out DNS-style records (A/AAAA/MX/TXT/CNAME/NS) at any useful scale
- makes TXT-verification churn (ACME, DKIM, DMARC) actively expensive
We want records to be:
- free to edit — no chain tx per change
- unlimited size — accommodate whole DNS record sets
- trustless — no operator can substitute their own records for the owner's
- backwards compatible — existing on-chain
h/s3/p/urecords keep working
Design
The chain says who owns the name. A signed manifest says what the owner's records are. Resolvers verify both.
The manifest — _records.json
Lives at the Sia bucket the name's on-chain records.s3 already points at,
under the fixed filename _records.json. Shape:
{
"v": 1,
"name": "bitcoin.cash",
"seq": 42,
"updated_at": "2026-09-12T00:00:00Z",
"dns": {
"A": ["1.2.3.4"],
"AAAA": ["2001:db8::1"],
"MX": [{"pref": 10, "host": "mail.example.com"}],
"TXT": ["v=spf1 include:_spf.silentmode.st -all"],
"CNAME": null,
"NS": []
},
"meta": { "note": "optional free-form" },
"sig": "H3n… (BCH message signature, base64)"
}
Rules:
v: schema version, currently1.name: the fully-qualified name the manifest belongs to. Resolver rejects a manifest whosenamefield doesn't match the URL it was fetched from.seq: monotonic integer. Resolvers cache the highest seen; blobs with an older or equalseqare rejected as replays.updated_at: ISO-8601 UTC. Human-readable only — verification is onseq.dns.*: DNS record slots.nullor[]means "not set".A/AAAA/TXTare string arrays.MXis objects{pref, host}.NSis a string array of host names. Additional slots (CAA,SRV) reserved for v2.meta: unstructured; ignored by DNS resolvers, useful for portal UI.sig: BCH-style message signature (same formatlibauth.signMessageproduces) over the canonical bytes of the envelope (see below).
Canonicalisation and signing
The signable bytes are the JSON serialisation of the manifest without the
sig field, with:
- keys sorted lexicographically at every level
- no insignificant whitespace (
JSON.stringifywith nospacearg) \u-escape every non-ASCII characternullfields OMITTED ("CNAME": nullbecomes absent)
The signature is sign(sha256(canonical_bytes)) with the wallet key whose
address currently holds the name's NFT certificate.
Verification, at the resolver:
- Read the name's chain record. Follow
records.s3to the Sia bucket. - Fetch
_records.jsonfrom that bucket. - Extract
sig, recomputesha256(canonical_bytes)over the rest. - Recover the signing pubkey → derive the CashAddress → compare to the current NFT owner address from the chain.
- Compare
seqto the cached last-seenseqfor this name. Reject if not strictly greater. - If everything matches: cache the new
seq, apply the DNS records.
Any step failing means "no records" — the resolver falls back to whatever
was already on the chain (h, s3 for content, etc.).
Endpoints (gateway)
GET /api/records/<name> — read-through of the signed manifest.
Returns 200 with the JSON if present, 404 if missing, 5xx on Sia errors.
Cacheable for a minute. Anyone can call this; it's public.
POST /api/records/<name> — write. Body IS the manifest JSON.
The gateway:
- Verifies the signature matches the current on-chain NFT owner.
- Verifies
seqis strictly greater than the last-seenseq(fromGETing the current manifest if any). - Uploads the JSON to the name's Sia bucket at
_records.json. - Returns 200 with
{seq, bytes, sia_key}.
Rate-limit: 1 write / 5 s per name. Refuses if the on-chain record has no
s3 pointer yet — the name has no storage location, no place to write.
Precedence and interaction with existing records
The signed manifest complements the on-chain records — it doesn't replace them:
- Content (
h,s3,p,u,ip) still comes from chain. - DNS records come from the manifest — chain has never carried them.
A future extension (v2) could allow the manifest to override h/s3/etc.
for owners who want everything off-chain. Not shipped in v1 because the
existing content model already works.
Threat model
- Operator tampers with the manifest. Signature verification catches it — the resolver rejects and falls back to on-chain records.
- Replay of an old manifest.
seqmonotonicity catches it — resolvers keep the highest-seenseq. - Owner's key compromised. Same as chain-side compromise: attacker controls the NFT and can sign whatever. Move the name to a new key.
- Sia object deleted. The name still resolves via chain records; DNS records disappear until the manifest is republished.
- Gateway hostility. The gateway signs nothing itself — it only relays
and enforces
seqmonotonicity. A hostile gateway can refuse to accept writes but cannot forge them; users can bypass it entirely by writing directly to Sia with their own credentials (v2 flow).
What ships in v1
- The spec (this doc).
- Gateway
/api/records/<name>(GET + POST) with signature verification and Sia read-through. - Portal DNS editor that composes a manifest, asks the wallet to sign, and POSTs to the gateway.
- Resolver support in Theseus + Ariadne staged behind a flag until the portal-side has been in production for a week.
What ships in v2
- Direct-to-Sia write flow for users who hold their own S3 credentials.
- Owner-controlled manifest that overrides
h/s3/etc. entirely. - CAA/SRV/HTTPS records.
- Federated resolver caches for
seq(mitigates the case where two writes race and each thinks itsseqis winning).