# Signed records manifest (BNS records v1) **Status:** shipped v1 — gateway `GET/POST /api/records/` and verified `GET /api/dns/` 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` / `u` records 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: ```json { "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, currently `1`. - `name`: the fully-qualified name the manifest belongs to. Resolver rejects a manifest whose `name` field doesn't match the URL it was fetched from. - `seq`: monotonic integer. Resolvers cache the highest seen; blobs with an older or equal `seq` are rejected as replays. - `updated_at`: ISO-8601 UTC. Human-readable only — verification is on `seq`. - `dns.*`: DNS record slots. `null` or `[]` means "not set". `A`/`AAAA`/`TXT` are string arrays. `MX` is objects `{pref, host}`. `NS` is 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 format `libauth.signMessage` produces) 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.stringify` with no `space` arg) - `\u`-escape every non-ASCII character - `null` fields OMITTED (`"CNAME": null` becomes 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: 1. Read the name's chain record. Follow `records.s3` to the Sia bucket. 2. Fetch `_records.json` from that bucket. 3. Extract `sig`, recompute `sha256(canonical_bytes)` over the rest. 4. Recover the signing pubkey → derive the CashAddress → compare to the current NFT owner address from the chain. 5. Compare `seq` to the cached last-seen `seq` for this name. Reject if not strictly greater. 6. 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/`** — 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/`** — write. Body IS the manifest JSON. The gateway: 1. Verifies the signature matches the current on-chain NFT owner. 2. Verifies `seq` is strictly greater than the last-seen `seq` (from `GET`ing the current manifest if any). 3. Uploads the JSON to the name's Sia bucket at `_records.json`. 4. 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.** `seq` monotonicity catches it — resolvers keep the highest-seen `seq`. - **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 `seq` monotonicity. 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/` (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 its `seq` is winning).