# 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` / `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/<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:
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/<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 its `seq` is winning).
