The addendum gains a section stating the rule that was implemented and why the contest window and the commit's lifetime differ from the first specification. The run records a front-runner registering a name first and losing it to the older commit.
140 lines
8.2 KiB
Markdown
140 lines
8.2 KiB
Markdown
# 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.
|
|
|
|
**REL** — one tx that:
|
|
1. Spends the name's certificate and does **not** recreate it (no output carries
|
|
the category): the certificate is burned.
|
|
2. Carries `OP_RETURN "BNS1{...}"` with `{"o":"REL","n":<name>}`.
|
|
3. Pays 600 sat to the beacon.
|
|
|
|
The name returns to the pool. Specified in `PROTOCOL-ADDENDUM-release.md`.
|
|
|
|
Transferring a name = sending the certificate to a new owner (no beacon tx needed).
|
|
A certificate moved that way must be anchored by an `UPD` before it can be released.
|
|
|
|
## 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)
|
|
|
|
**One message per transaction.** A transaction's message is the one output
|
|
that parses as a BNS payload with a known operation. If more than one output
|
|
does, the transaction carries **no** message. *(2026-10-04. Before, the last
|
|
one won — which let whoever assembles a transaction replace a message another
|
|
party had fixed in it. No transaction on chipnet carried more than one.)*
|
|
|
|
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** — with one
|
|
refinement since 2026-10-05: a `REG` may be the reveal of an earlier `CMT`
|
|
(it carries the salt `s`), and for six blocks after a name's first valid
|
|
`REG` one with an older commit takes the name instead. Optional on chipnet,
|
|
required on mainnet. See `PROTOCOL-ADDENDUM-commit-reveal.md` §0. The certificate
|
|
must be the **only token of its category**: the category is created by
|
|
this transaction (it spends, at output index 0, the outpoint whose txid is
|
|
the category), exactly one output carries it, and that output is an NFT
|
|
with no capability and no fungible amount. Rule 4 accepts an update from
|
|
any token of a name's category, so a registration that left a second one
|
|
in existence would let its holder rewrite the name after selling the
|
|
certificate; such a registration is void. *(Added 2026-10-04; every valid
|
|
registration on chipnet already satisfied it.)* If the name was
|
|
released (rule 5), the `REG` must be in a later block than the confirmed
|
|
release — the one-block gap.
|
|
4. `UPD`: valid if some output of the tx carries the name's category.
|
|
A name whose certificate is held by the registry covenant answers only to
|
|
an `UPD` that states the covenant state (`c`) and whose claim matches the
|
|
certificate's address; it stops resolving once its grace period has
|
|
passed. See `PROTOCOL-ADDENDUM-covenant-names.md`. *(Chipnet, 2026-10-04.)*
|
|
5. `REL`: valid if the name is registered, no output of the tx carries the
|
|
name's category, and one input spends an output of an earlier beacon
|
|
transaction that carries the certificate. The name leaves the index.
|
|
6. 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.
|
|
|
|
## Blocklist (resolver-side)
|
|
|
|
Not a chain rule. A resolver may subscribe to lists of flagged names and warn
|
|
about, or refuse, a name a list flags; the name stays registered and any
|
|
resolver that does not subscribe resolves it as before. The messages (`RPT`,
|
|
`BLK`, `UBK`), the authority rule and the client behaviour are in
|
|
`PROTOCOL-ADDENDUM-blocklist.md`.
|
|
|
|
## 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 until their owner releases them (`REL`);
|
|
an abandoned name still lives forever (Namecoin used ~8-month renewals to
|
|
garbage-collect dead names — the covenant registry is that step).
|
|
- 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).
|