argus/PROTOCOL.md

96 lines
5.5 KiB
Markdown
Raw Normal View History

# BNS1 — a minimal Namecoin-style name protocol on Bitcoin Cash (chipnet prototype)
Names are anchored by **CashTokens name certificates** on the BCH chain; records travel in
**OP_RETURN** payloads; discovery uses a **beacon address** so a plain electrum
server is the only infrastructure a resolver needs. No fork, no sidechain.
## Roles of the three mechanisms
| Mechanism | Role |
|---|---|
| CashTokens certificate (immutable NFT) | *Ownership.* The certificate's commitment is the UTF-8 name. Whoever holds the certificate owns the name. Consensus guarantees nobody can mint an output of an existing category without spending it. |
| OP_RETURN payload | *Data.* `BNS1` prefix + JSON: the name's records (URL, inline HTML page, payment address). |
| Beacon dust output | *Discovery.* Every protocol tx pays 600 sat to the well-known beacon address, so `blockchain.address.get_history(beacon)` enumerates every registration/update ever made. |
**Beacon (chipnet):** `bchtest:qp8njaa9a3ahg7259s5qajnce526n0rttsq8qwx4jl`
## Transactions
**REG** — one tx that simultaneously:
1. Mints an immutable certificate, commitment = UTF-8 name (≤ 40 bytes). Its category
(= token ID) becomes the name's permanent identity.
2. Carries `OP_RETURN "BNS1{...json...}"` with `{"o":"REG","n":<name>,"r":<records>}`.
3. Pays 600 sat to the beacon.
**UPD** — one tx that:
1. Spends (moves) the name's certificate back to the owner — this is the ownership proof.
A vout carrying the name's category is only possible if a vin held it (consensus).
2. Carries `OP_RETURN "BNS1{...}"` with `{"o":"UPD","n":<name>,"r":<newRecords>}`.
3. Pays 600 sat to the beacon.
Transferring a name = sending the certificate to a new owner (no beacon tx needed).
## Records (JSON, keep total payload ≤ 200 bytes)
- `u` — https URL the name points to (gateway redirects)
- `h` — inline HTML: a tiny website stored fully on-chain
- `a` — cashaddr for payments (the "send to alice.bch" use case)
- `ip` — IPv4 of a server that answers for the .bch host directly (DNS answers
this instead of the local gateway)
- `p` — upstream URL the local gateway reverse-proxies (address bar stays .bch)
- `s3` — `bucket/key` (single object) or `bucket/prefix/` (site root) stored on
the Sia network, served by the gateway through a local s3d daemon
(credentials in sia-s3.json, not on-chain). Storage is decentralized and
encrypted; the retrieval door is the local daemon.
- `tls` — SHA-256 fingerprint (hex, over DER) of the name's TLS certificate.
The chain replaces the CA: identity comes from the certificate holder publishing
this record, encryption from ordinary TLS. Resolvers/extensions can pin it;
the prototype's trust path is the BNS Local Root CA (see ca/) which signs
per-name certs and is installed as a trust anchor by setup/install.ps1.
## Resolver rules (indexer-enforced)
1. Fetch beacon history, sort by `(height, txid)`; unconfirmed last.
2. **Then reorder causally:** never index a transaction before one it spends
from. Independent transactions keep the `txid` tiebreak. *(See "Ordering"
below — this rule is load-bearing, added 2026-07-26.)*
3. `REG`: valid if the same tx mints a certificate whose commitment matches the name
and the name is not yet taken. **First valid REG wins.**
4. `UPD`: valid if some output of the tx carries the name's category.
5. Names: `[a-z0-9-]{1,32}`, rendered as `<name>.bch`.
### Ordering (why rule 2 exists)
`(height, txid)` alone is arbitrary within a block and within the mempool, and
that breaks the most ordinary sequence in the protocol: **register a name, then
immediately point it somewhere.** If the `UPD`'s txid happens to sort below its
own `REG`'s, the `UPD` is evaluated against an index that does not contain the
name yet, fails rule 4, and is silently dropped — *permanently*, if both land in
the same block, since the ordering never changes afterwards.
Observed on chipnet 2026-07-26: `REG ca243c06…` for `smtest-fbe8cd.p2p` and its
`UPD 8da2cc4a…` were both unconfirmed; `8da2…` sorted first, and the record
never appeared even though the update transaction was entirely valid.
The fix is a stable topological pass over the history: keep the `(height, txid)`
baseline, but hold back any transaction whose parents (within the beacon set)
have not been emitted yet. Cost is O(n²) over the beacon history, which is read
once per index build. Implemented identically in `src/lib/resolver-web.js` and
`src/lib/bns.js` — **the two must stay in lockstep**, since a disagreement
between them is a consensus split between clients.
## Honest limitations (prototype vs production)
- Uniqueness is **indexer policy**, not consensus. Production: a covenant-run
registry (CashScript) that consensus-enforces one certificate per name, or full
BCMR-style client-side validation of the genesis chain.
- Intra-block ordering uses txid as tiebreak among *independent* transactions
(electrum history has no tx index). Dependent transactions are ordered
causally — see "Ordering" above.
- The beacon is a keyed address (prototype convenience); production would use a
keyless constant script so no one can be pressured over it.
- No expiry/renewal: names live forever once minted (Namecoin used ~8-month
renewals to garbage-collect dead names).
- Front-running: a miner/mempool observer could see a REG and race it
(Namecoin's two-step `name_new` commit-reveal solved this; omitted here).