Signed On-Chain Reachability Manifest
A service can keep a domain name, an app-store listing, or a hosting account taken away from it by a third party, even while the service itself keeps running fine. When that happens, users and apps that only knew how to find it through that domain or listing simply lose it, with no way back in. This describes a way for a service to publish, somewhere nobody can take down, a list of every place it can still be reached.
Reference for an AI
Everything an assistant needs to answer questions about BRC-211 accurately, including what it depends on.
Summary
- Why
- A domain can be suspended, an app delisted, or a host seized, cutting off access to a service even though the service itself still runs somewhere.
- What
- BRC-211 is a signed record, published on the BSV blockchain, listing the transports (onion, clearnet, IP, IPFS, Nostr, inscription) where a service can be reached, verified by a public key compiled into the client.
- How
- A client holds the issuer's public key as a build-time constant, fetches candidate manifest tokens from any source, verifies each one's ECDSA signature over a canonical JSON encoding, keeps the highest sequence number at or above its stored floor, and tries the listed endpoints in priority order until one connects.
What this lets you do
- Publish a signed list of reachable endpoints on chain
- Recover a service's location after a domain, host, or app-store takedown
- Verify a manifest with one SHA-256 hash and one ECDSA check
- Order endpoints by priority and transport kind before connecting
- Reject replayed or stale manifests using a rising sequence floor
Written by claude-sonnet-5 from the specification text. Where the two differ, the original is correct.
The specification
Abstract
This BRC defines a reachability manifest: a record, signed by a service's issuer key and published on the BSV chain, that lists the transports at which the service can be reached (an inscribed application shell, Tor onion services, clearnet mirrors, IP addresses, IPFS content, Nostr relays), each with a priority, under one sequence number. A client holds the issuer key as a constant compiled into its build, takes candidate manifests from any source, accepts only those whose ECDSA signature over a domain-separated canonical JSON encoding verifies under that key, keeps the one with the highest sequence number at or above its stored floor, and tries its endpoints in order. It can therefore still find the service after a domain suspension, a host seizure or an app-store removal. Presence on chain confers no trust, and the signature proves who listed the endpoints, not that the list is the newest or that each endpoint is still the service's. Overlay discovery (BRC-88) takes over once a connection exists.
Motivation
BRC-22, BRC-24 and BRC-88 assume that the client can already reach an overlay host. That host, like a service's own front end, is found through a domain name, an API origin or an app-store listing, each of which a third party can remove while the service keeps running: a registry can put a domain on serverHold, which under RFC 5731 stops its delegation being published in DNS, a store can delist an app, and a provider can null-route an address. What is missing is a bootstrap step whose trust root and record both survive those actions. No registrar or store can withdraw a mined transaction, so this BRC uses the chain as the place where a service publishes and the client build as the place where trust is fixed. A client needs one SHA-256 hash and one ECDSA verification per candidate, and one stored integer.
Scope and conformance
§§2 to 15 specify the manifest and its verification, the on-chain forms, endpoint ordering, the build pin, the HTTP binding of a token source and versioning; §16 lists every requirement by role. Out of scope are how a source finds tokens on chain, the protocol spoken to an endpoint, authentication of the service once connected, issuer key generation, and any overlay topic manager or lookup service.
A deployment that follows this BRC defends against the removal of a name, host, address or store listing (provided the client can still reach a token source and one listed endpoint), against forged manifests, against replay of a genuine manifest older than one the client has accepted while its storage keeps the floor, and against a party that writes client storage to replace the issuer key. It does not defend against staleness, theft of the issuer key, a listed endpoint that is dishonest or has changed hands, tampering with the client's own code, a censor that blocks the listed endpoints, or disclosure of the manifest, which is public.
The key words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in capitals. They describe conformance to this BRC. Implementations and compatibility notes records how the listed implementations behave where this BRC leaves behaviour open.
Relationship to existing standards
This standard answers one question: given only a public key compiled into a client, where can that client reach the service the key speaks for?
Overlay discovery: BRC-22, BRC-24, BRC-88 and BRC-101
BRC-22 and BRC-24 define how an overlay host admits transactions and answers lookups, BRC-88's SHIP and SLAP advertisements are resolved through SLAP trackers at HTTPS base URLs, and BRC-101 describes richer advertisement URLs and calls itself aspirational. All of them begin once the client reaches an overlay host, the step a manifest supplies. Trust differs too: an advertisement is attributable to whichever key publishes it, and BRC-88 does not limit who may advertise a topic, while a manifest is accepted only under a key the client held before it ran.
A signed host list shipped with the client: BRC-178
BRC-178, on bootstrapping and default SLAP trackers, recommends that clients ship a list of overlay host URLs and requires that the list be signed by a publisher key shipped with the client or retrieved over a pinned channel, that it carry a version and a fetched-at time, and that the user can replace it, leaving format, transport and verification open. A manifest meets the first of these requirements as written and half of the second: seq is a version, and updated_at is informational (§2), not a fetched-at time. A tracker list could be carried as a manifest under its own service id. A user override chooses where to connect, which this standard leaves alone; §11 forbids only a stored value that changes which signatures the client believes.
Discovery through a domain: BRC-68, BRC-180 and BRC-169
BRC-68 publishes an entity's key in a domain's /manifest.json, BRC-180 adds overlay service URLs there and calls the declaration secured by control of the domain, and BRC-169 resolves @handle@domain to an identity key and messagebox URL at the handle's domain. Each takes its trust root, and BRC-180 and BRC-169 their endpoints, through DNS, which lets the domain holder change keys by editing a file and which a serverHold or a host seizure removes. This standard compiles the key into the client (§11) and takes endpoints from any source, at the price of rotation by new build; a client can follow BRC-180 while the domain resolves and fall back to a manifest when it does not. BRC-169's Reachability Policy governs who may message a handle; here the word means only where a service can be contacted.
The same pin discipline: BRC-179
BRC-179 pins a capability manifest's fingerprint into the build and, in its section 9, forbids reading the pin from runtime-mutable state in a production build. §11 applies that discipline to a signing key, and BRC-179's stated limit holds here too: a pin gives no protection against an attacker who already controls the host or the build.
Other signed overlay records: BRC-77, BRC-192, BRC-194 and BRC-183
BRC-192 signs an ASCII type prefix, a 0x00 byte and the RFC 8785 (JCS) serialization of a body under BRC-77, with a key derived under BRC-42 and BRC-43, as BRC-194 does for a host's expiring capability manifest. §§5 and 6 have the same shape (Claim 2), but sign with the pinned key directly, so a reader needs only SHA-256 and secp256k1, and carry no expiry, which during a takedown would leave every client without a record. BRC-183 observes that a web endpoint is not automatically an overlay; this standard defines no topic manager and is filed under Overlays because it supplies the step before overlay discovery.
Prior art outside the BRC series
The Update Framework (TUF) keeps root keys offline, refuses older metadata and expires every file. The floor of §8 answers its rollback attack for a client that has seen the newer manifest; its freeze attack, which TUF answers with expiry, is not answered here; and its fast-forward attack applies, since a raised floor cannot be lowered. TUF rotates its root in band, where this standard rotates by new build.
Certificate Transparency (RFC 9162) makes misissuance detectable. The chain gives published manifests the same public record, though §7 never asks whether a token was published.
DNSSEC (RFC 4033) authenticates what a name says, and cannot keep a name in existence.
IPNS records carry a sequence number, an expiry and a TTL under the signed prefix ipns-signature:, and lapse from the DHT within 48 hours unless republished. This standard shares the sequence rule and the signed prefix, keeps the key in the build instead of the name, needs no republishing, and has no expiry.
Nostr relay lists (NIP-65) are the nearest prior art for publishing a key's endpoints through untrusted servers. NIP-01 keeps the latest replaceable event by created_at, ties going to the lowest id. A manifest is ordered by an issuer-assigned seq, so no clock can reorder it, and no rule chooses between two valid manifests with one seq (§8).
Specification
1. Terminology and roles
- Issuer: the identity that signs a service's manifests and holds the issuer private key, distinct from any wallet key that pays for on-chain outputs.
- Issuer key: the issuer's secp256k1 public key, written here in compressed form as 66 lowercase hex characters; the client's trust root.
- Service id: a short ASCII label naming the service, such as
"messaging". One issuer key MAY sign for several services, and a client always verifies against the service id it expects. - Manifest: the signed record of a service's endpoints at one sequence number. Token: its string form (§4).
- Endpoint: one
(kind, url, priority)entry; itsurlis its locator. - Candidate: any string presented to selection (§8) as a possible token.
- Floor: the highest sequence number a client has accepted (§8).
Requirements are stated for five roles, and one party may play several.
| Role | What it does | Sections |
|---|---|---|
| Signer | builds a payload and signs it under the issuer key, normally on an offline machine | §§2 to 6, 8, 11 |
| Publisher | writes a token on chain, paying with a funding key of its own | §9 |
| Token source | supplies candidates: a node that serves the tokens it holds (§13), an index that records the outputs of §9, or a client reading transactions itself | §§9, 13 |
| Verifier | checks one token under a given issuer key | §§4 to 7 |
| Client | holds the pinned key and its floor, selects the current manifest, orders its endpoints and connects | §§8, 10 to 12 |
2. Payload object
The payload is a JSON object. A signer following this BRC emits exactly these members:
| Member | JSON type | Required | Meaning |
|---|---|---|---|
v | integer | yes | Format version. A signer following this BRC writes 1. |
service | string | yes | Service id. MUST be non-empty. |
seq | integer | yes | Sequence number, >= 0. The ordering authority (§8). |
updated_at | integer | yes | Publication time in epoch seconds. Informational only: ordering uses seq, never updated_at. |
endpoints | array | yes | One or more endpoint objects. |
Each endpoint object has these members:
| Member | JSON type | Required | Meaning |
|---|---|---|---|
kind | string | yes | One of the kinds in §3. |
url | string | yes | Transport-specific locator (§3). MUST be non-empty. |
priority | integer | no (default 100) | Try-order hint. Lower is tried first. Any integer within the bound below is valid, a negative one included. |
A later version MAY add members. It MAY also add a top-level integer min_reader, the lowest reader version that is allowed to use the manifest (§7, step 4). A verifier MUST ignore members it does not recognise; the signature still covers them, because canonicalization (§5) takes in every member of the decoded object.
A signer MUST reject a payload that contains a non-integer number anywhere, since floating-point values serialize differently across languages and would break byte parity between signer and verifier. A signer MUST also reject a payload that contains an integer whose absolute value exceeds 9007199254740991 (2^53 - 1), beyond which a JavaScript number no longer holds every integer exactly.
Strings SHOULD be printable ASCII, so that an operator approving a manifest sees exactly what each locator names (Locator confusion). Canonicalization handles any other character.
A conforming signer never produces a payload whose required members are missing or carry another JSON type. This BRC does not define how a verifier treats such a payload when it is correctly signed, except for v and min_reader (§7, step 4); the listed readers refuse it (compatibility note 1). Nor does a verifier check the signer's other rules: it accepts a negative seq, an empty endpoints array, which leaves the resolver nothing to try, and an empty url.
3. Endpoint kinds and preference order
A signer MUST NOT emit any kind other than the six below. Each has a fixed rank, which breaks ties between endpoints of equal priority (§10).
kind | Rank | What url names | Takedown posture |
|---|---|---|---|
bsv | 0 | The client application inscribed on chain as a 1-sat ordinal: an inscription id (<txid>_<vout>) or a txid. Never an HTTP URL. | Immutable and outside every registry. A gateway that serves it can be seized, or can serve other bytes to a client that does not check them. Usable only by a client that can fetch inscriptions. |
onion | 1 | A Tor v3 onion service. | Self-authenticating, since the address encodes the service's key, and outside every registry. |
https | 2 | A clearnet host, such as a hardened domain or an alternate-TLD mirror. | Exposed to registry and DNS action, and to anyone who obtains a certificate for the name after it changes hands. |
ip | 3 | A raw IP address. | Needs no DNS. The manifest binds no key to it, and its address can be null-routed or reassigned. |
ipfs | 4 | A content identifier. | Content-addressed: the identifier authenticates the content only if the client checks it. |
nostr | 5 | A Nostr relay. | Usually named by a domain, with the exposure of https. |
A verifier does not reject a manifest for an unknown kind: it ranks the endpoint 99 (§10), and a client that passes allowed_kinds never tries it.
A signer SHOULD include at least one onion endpoint, the only live kind both outside every registry and self-authenticating. Publishing without one is permitted, because some services are reachable only by IP, but the signer SHOULD warn the operator when it happens. An ip endpoint SHOULD be treated as lower assurance: the manifest binds no key to it, and over plain HTTP or WebSocket it belongs to anyone on the path.
Locators. kind names the network, and locator syntax is left to the publisher and its clients. In the listed applications a locator is a host with an optional :port and path and no scheme, to which the client adds https or wss for https, http or ws for onion and ip, and wss for nostr. Nothing ties a locator to its kind at verification (Locator confusion). The vectors' example.onion is a placeholder, not a version 3 address.
4. Token serialization
A token is an ASCII string with two parts separated by a dot:
token = BASE64URL_NOPAD(wire_json) "." DER_SIG_HEX
wire_jsonis the payload as UTF-8 JSON, whose member order and whitespace do not affect verification (§5). The reference signer writes compact JSON with members in the orderv, service, seq, updated_at, endpointsandkind, url, priority, and every non-ASCII character as a\uescape; a signer that wants to reproduce the vector tokens uses the same layout.BASE64URL_NOPADis the URL-safe base64 alphabet of RFC 4648 §5 with the trailing=padding removed. A decoder MUST accept the unpadded form, restoring the padding first ("=" * (-len % 4)) if its library needs it.DER_SIG_HEXis the DER-encoded ECDSA signature of §6 in lowercase hex.
A token is split at its last ., and a string with no . is not a token. The listed readers accept different supersets of this grammar, each variant decoding to the same wire JSON and signature or failing, so that a token stored with a trailing line ending, for example, verifies in some readers and not in others (compatibility note 3).
5. Canonicalization
The signed bytes are computed from the decoded payload object, so they do not depend on the layout of the wire JSON. For a payload object P:
canonical(P) = UTF-8 bytes of the JSON serialization of P, with
- object keys sorted in ascending order, recursively (inside endpoint objects too)
- no whitespace: "," between items, ":" between key and value
- non-ASCII characters written as UTF-8, not escaped
- no NaN or Infinity, and no non-integer numbers
The rule is Python's json.dumps(P, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False).encode("utf-8"). Strings are escaped as JSON requires: " and \ are escaped, U+0000 to U+001F are written as \b, \t, \n, \f and \r where those exist and as \u00xx with lower-case hex digits otherwise, and every other character, U+007F, U+2028 and U+2029 among them, is written as its UTF-8 bytes. Canonical bytes therefore never contain a raw 0x00.
Member names MUST be ASCII, in this and every later version, because Python sorts by code point and JavaScript by UTF-16 code unit, and the two orders can differ outside ASCII. A JavaScript implementation MUST sort recursively; a shallow sort leaves endpoint members in wire order, and its digests do not interoperate. On every payload that meets §2 and whose strings are sequences of Unicode scalar values, canonical(P) is byte for byte the output of the RFC 8785 serialization steps, and it is the RFC 8785 canonical form itself when no string holds a Unicode noncharacter, which the I-JSON input rule of RFC 8785 excludes (Claim 2).
A verifier MUST canonicalize the same decoded object that it then reads fields from. Whatever the wire spelling, the decoded object then canonicalizes to the bytes the issuer signed or fails verification, barring a forgery, and for a payload a conforming signer produces the fields read mean what the issuer signed (Claim 4). A verifier is not required to check the integer bound or the ASCII rule; a payload that breaks either, which only a holder of the issuer key can produce, can verify in one reader and fail in another. Vector 1 gives a worked example.
6. Signature scheme
Let DOMAIN be the 19 ASCII bytes bsv-reachability-v1. The signed bytes and the digest are:
signed = DOMAIN || 0x00 || canonical(P)
digest = SHA256(signed)
sig = ECDSA_sign(issuer_priv, digest) secp256k1, DER-encoded, low-S
Neither the domain nor canonical bytes contain 0x00, so the separator is the only zero byte in signed, and the domain confines a signature to this protocol (Claim 5). The digest is a single SHA-256 of signed, not a double SHA-256 and not the Bitcoin Signed Message construction.
A signer MUST produce low-S signatures, that is, with s <= (n - 1) / 2 where n is the order of the secp256k1 group. The reference signer derives its nonces by RFC 6979 §3.2 (HMAC-SHA-256, no additional data) and normalizes s to low-S, so a signer built the same way reproduces the vector signatures exactly. A verifier MAY reject a high-S signature, and the listed readers differ on this. A high-S signature is the malleated twin of a low-S one over the same payload, so when the low-S original is also presented it cannot change which payload is selected; a source that serves only the twin withholds the manifest from a reader that refuses high-S (compatibility note 3).
Verification is:
digest = SHA256(DOMAIN || 0x00 || canonical(P))
valid = ECDSA_verify(issuer_key, DER_decode(DER_SIG_HEX), digest)
7. Verification algorithm
verify_manifest(token, issuer_key_hex, expected_service?, min_seq?) -> (ok, manifest?, reason)
Verification MUST NOT throw or raise. Every failure returns ok = false with a reason. The steps, in order:
- If
tokenis absent, empty or contains no., return(false, none, "no manifest presented"). - Split at the last
., base64url-decode the first part, parse it as JSON, require an object, and read the payload members. On any failure return(false, none, "malformed manifest"). - Parse
issuer_key_hexas a public key (§1), computedigestas in §6, decode the signature from its hex and DER form, and verify it against the key. If the key is not a curve point, the signature cannot be decoded, or verification fails, return(false, none, "manifest signature does not verify"). - Let
Vbe the highest format version the verifier implements, which is 1 for this BRC. Ifvis missing, is not an integer or is below 1, return(false, none, "unsupported manifest version"). Ifmin_readeris present and is not an integer or is greater thanV, return(false, none, reason)with a reason saying that a newer reader is required. Otherwise continue, reading only the members this BRC defines, whatever the value ofv. - If
expected_serviceis given andservicediffers from it, return(false, manifest, "manifest is for a different service"). - If
min_seqis given andseq >= min_seqdoes not hold, return(false, manifest, "manifest is older than the last-seen (rollback rejected)"). Aseqequal tomin_seqpasses, and against amin_seqthat is not a number, such asNaN, none passes. - Otherwise return
(true, manifest, "valid").
Step 3 precedes every semantic check, so every field inspected after it is authenticated. Steps 5 and 6 return the parsed manifest for diagnosis; steps 1 to 4 return none. In step 4 a JSON true, a numeric string and null are not integers, and a present min_reader of null is refused, not treated as absent.
Step 4 keeps an upgrade from stranding installed clients. A version 2 manifest MAY add members, and a version 1 reader that accepts it reads the members it knows, which stays correct while the change is additive; for a change that is not, the publisher sets min_reader inside the signed payload and older readers refuse the manifest.
8. Sequence floor and highest-valid-seq selection
seq is the only ordering authority. A client persists the highest seq it has accepted and passes it as min_seq, and on accepting a manifest with sequence s it sets min_seq = max(min_seq, s), so the floor only rises and a genuine manifest older than one the client has accepted is never selected again while its storage keeps the floor. A client with no floor, such as a fresh install, accepts any genuine manifest ever signed for the service. A seq equal to the floor is accepted, so that a restarting client can read the current manifest again.
select_current(tokens, issuer_key_hex, expected_service, min_seq?) verifies each candidate with the given expected_service and min_seq and returns the valid manifest with the greatest seq, or none. expected_service is a required argument in every listed implementation, since a verifier called without it would accept another service's manifest signed under a shared key. A forged manifest fails its signature, and an older genuine one loses to any newer genuine candidate presented; selection cannot prefer a manifest that no source presents.
A signer MUST increase seq on every change. Reusing a seq lets both manifests pass the floor, and when two valid candidates share the greatest seq the listed implementations keep the first they encounter, so discovery order decides. Whether two candidates are different manifests is a question about their canonical payloads (§5), never about token text: a high-S twin and a re-encoded wire JSON are the same manifest.
9. On-chain publishing and discovery
A token is written on chain in one of two forms. Either form MAY be accompanied, in the same transaction, by a constant marker output.
Carrier form (default). An OP_RETURN output:
OP_FALSE OP_RETURN "TKRM1" <token>
bytes: 0x00 0x6a PUSH("TKRM1") PUSH(token as ASCII)
Ordinal form (permanent). A 1-satoshi output whose script is a P2PKH lock followed by a 1Sat Ordinals inscription envelope with content type text/plain and the token as its body:
<P2PKH lock> OP_FALSE OP_IF PUSH("ord") OP_1 PUSH("text/plain") OP_0 PUSH(token as ASCII) OP_ENDIF
A publisher MUST put the whole token in that single body push. A 1Sat Ordinals decoder takes the one push after OP_0 as the whole body, so a split token reaches it truncated and never verifies, and since the Genesis upgrade no 520-byte limit requires splitting. A reader MAY still accept a body split across several pushes, concatenating them in order. The output stays spendable by its owner.
Discovery marker (optional). A constant output that a publisher MAY add in either form:
OP_FALSE OP_RETURN "TKRMREG1"
bytes: 0x00 0x6a 0x08 54 4b 52 4d 52 45 47 31 = 006a08544b524d52454731
PUSH(data) writes the length of data using the first of these encodings that fits:
Length n | Encoding |
|---|---|
n < 0x4c | [n] then data |
0x4c <= n <= 0xff | 0x4c [n] then data (PUSHDATA1) |
0x100 <= n <= 0xffff | 0x4d [n_lo n_hi] then data (PUSHDATA2, little-endian length) |
n > 0xffff | 0x4e [4-byte little-endian length] then data (PUSHDATA4) |
A token is normally a few hundred bytes; the Vector 1 token is 613 bytes and goes in a PUSHDATA2.
Marker script hash. The lookup key is SHA256(marker_script) with its 32 bytes reversed, in hex, the script-hash convention of the ElectrumX protocol. It is the same for every service and issuer, and a client MAY hard-code it.
marker_script = 006a08544b524d52454731
marker_script_hash = 6f33820d5cb3f85c23bdd46daeea10bc197c3ef2c93f9a51b6e2e5bdedab49ce
A marker lookup needs an index that keeps data-carrier outputs, and this BRC assumes none: an index of spendable outputs, such as the original ElectrumX server, which skips every output beginning OP_FALSE OP_RETURN, does not hold the marker. A client MAY query an index that records those outputs under their script hash, but MUST NOT depend on the marker as its only source of candidates (Security Considerations, Adversaries).
Extracting candidate tokens. An implementation that reads a transaction collects candidates from every output:
- an output that begins
OP_FALSE OP_RETURNand whose first push isTKRM1: the push that follows, decoded as ASCII; - an output carrying an inscription whose content type begins with
text/plain: the body, its pushes concatenated if split, decoded as ASCII.
A candidate that is not valid ASCII is not a token. It MUST be skipped, never accepted, and it is never fatal. Every candidate collected goes to select_current (§8). Compatibility note 8 records how the reference parses these forms.
Trust model. Anyone can publish stale or forged manifests in either form, with or without the marker. A client MUST NOT treat "found on chain" as evidence of anything; a manifest is trusted only if it verifies under the pinned key (§7) and wins selection (§8). The issuer key, which signs, and the funding key, which pays for and owns the output, are deliberately different keys.
Broadcast uncertainty. A publisher that does not know whether its broadcast succeeded MUST NOT simply broadcast again, since a resend can inscribe the ordinal twice. It MUST report the transaction id it built, so that an operator can check the chain first. Readers are unaffected by duplicates.
10. Endpoint ordering and the fallback resolver
Given a verified manifest and an optional set allowed_kinds of the transports the client can use (a client without Tor omits onion; one that cannot fetch inscriptions omits bsv), the try-order is:
ordered = [ e for e in endpoints if allowed_kinds is None or e.kind in allowed_kinds ]
sorted by (e.priority ascending, RANK(e.kind) ascending, e.url ascending)
RANK is the rank of §3, and 99 for an unknown kind. Ties left after priority and rank are broken by url, compared by code point in the reference and by UTF-16 code unit in the JavaScript readers, which agree on ASCII. Endpoints that tie on all three keys keep their emit order, since the listed sorts are stable.
The filter runs before any probe, and nothing adds a kind to allowed_kinds, so failures cannot move a client onto a kind it excluded. A privacy-sensitive client, for example one in a Tor-only mode, MUST leave bsv and every clearnet kind out of allowed_kinds and keep to onion; fetching an inscribed application usually goes through a clearnet gateway.
A FallbackResolver is built from a verified manifest and allowed_kinds and holds the ordered list. resolve(probe) returns the first endpoint for which probe(endpoint) is true. An exception thrown by probe MUST be treated as that endpoint failing; the resolver catches it and moves on. If every endpoint fails, resolve returns none and never substitutes an endpoint of its own. That is a total failure, and the caller MUST handle it; both listed applications keep their default connection.
11. Issuer key pinning: the build-pin rule (normative)
The pinned issuer key is the whole trust root, so this BRC requires a build pin that configuration cannot override.
- The issuer key MUST be shipped as a constant compiled into the distributed bundle or binary, inlined at build time. It comes from the offline ceremony described below.
- A client MAY honour a locally stored key (a configuration or
localStoragevalue) only in a development build that carries no pin. Whether a build is a development build MUST be fixed when it is built, by a flag that no runtime state can change. A production build MUST NOT honour a stored key, with or without a pin. - When a build pin is present, a stored value MUST NOT be able to substitute a different key, in a development build or any other. The resolution rule, where
dev_buildis that build-time flag, is:
resolve_issuer_key(pinned, stored, dev_build):
p = trim(pinned)
s = trim(stored)
if p != "":
if s != "" and lowercase(s) != lowercase(p):
return { key: "", conflict: true } # the stored value tried to replace the pin: refuse
return { key: p, conflict: false } # the build pin is authoritative
if dev_build:
return { key: s, conflict: false } # development build with no pin: the override is allowed
return { key: "", conflict: false } # production build with no pin: inert, whatever is stored
The comparison ignores hex case. When conflict is true the client MUST refuse discovery, MUST log the refusal as possible tampering, and MUST keep its default connection (its default relay or same-origin API). It MUST NOT go on with the stored key. Without this rule, anything able to write client storage, such as an information stealer or an injected script, could point discovery at its own issuer key, and the redirection would outlast the foothold that wrote it; a production build that honoured a stored key while its pin was empty would open the same door. An empty pin in a production build makes discovery inert instead, the safe shipped state until an operator pins the real key. Rotating the issuer key means shipping a new signed build.
The rule protects the key and nothing else: the floor, the token source and a privacy mode are ordinary stored settings (Security Considerations, Adversaries), and a tampered build, or a web client loaded from a seized origin, carries whatever pin the attacker chose.
Issuer key custody. The private issuer key MUST be generated and kept offline, in a ceremony, and MUST be independent of any wallet or funding key. It SHOULD sign nothing except records that carry their own domain separator: a tag that contains no 0x00 byte followed by a 0x00 byte, as in §6, or the Bitcoin Signed Message construction, whose signatures, made over a double SHA-256, cannot be replayed as manifests short of a SHA-256 collision or an event rarer still (Claim 5). A key that hashes and signs bytes chosen by someone else could be handed DOMAIN || 0x00 || canonical(P) and would return a valid manifest.
12. Client bootstrap flow (informative)
- Resolve the effective issuer key with the build-pin rule (§11). If no key is usable, stay on the default connection and stop.
- Obtain candidate tokens from a token source (§13), normally a node or overlay host that serves them. A client MAY also query the marker script hash (§9) at an index that records data-carrier outputs; this BRC names no such index, and the marker is never the only source. A client in a Tor-only mode fetches candidates only from an onion source, judged by the host of the parsed URL.
- Call
select_current(candidates, issuer_key, expected_service, min_seq = stored floor)(§§7 and 8). - If a manifest is selected, raise the stored floor to its
seq. Raising it at selection, before any probe, keeps the floor's protection in a session in which no endpoint answers (Claim 6). - Build a
FallbackResolver(manifest, allowed_kinds), withallowed_kindsset by what the client can do and its privacy mode (§10). - Call
resolve(probe)and connect to the first working endpoint, or take the first endpoint of the ordered list if the connection layer already retries. If every endpoint fails, stay on the default connection.
13. Token sources and the HTTP binding
A token source is any party that supplies candidates, such as a node that holds the tokens its operator has published, a node that scans blocks for the forms of §9, or an index that answers the marker script hash. Every candidate is verified under the client's pin, so a source can withhold or reorder manifests and cannot forge one.
A host that serves candidates over HTTP answers GET <base>/reachability, where <base> is the configured URL with one trailing / removed. A successful answer has status 200 and a body that is a JSON object whose member tokens is an array of token strings, not filtered by service or issuer. A host MAY add other members; the listed host adds markerScriptHash, the §9 value. A browser client on another origin can read the answer only if the host sends Access-Control-Allow-Origin.
A client treats a status outside 200 to 299, a body that is not a JSON object, or a missing or non-array tokens member as an empty list. It drops entries that are not strings and passes the rest to select_current unchanged, without trimming. A failure to connect, or any other error while fetching, also gives an empty list, and the client keeps its default connection.
14. Versioning and extensibility
This BRC has three version markers, each changing for a different reason.
- The signing domain,
bsv-reachability-v1, names the construction of §§5 and 6 and changes only if the construction does, a new domain then keeping old and new signatures apart (Claim 5). Payload versions do not change it: the version 2 tokens of Vector 4 use the same domain. - The payload version,
v, withmin_reader, governs what the members mean (§7, step 4). An additive change raisesv; a breaking change also setsmin_reader. - The on-chain tags,
TKRM1andTKRMREG1, are opaque byte strings compared byte for byte, and change only if an on-chain form changes in a way an existing reader would misread.
A later version can add payload members, which version 1 readers ignore, and endpoint kinds: the kind list is closed for version 1 signers (§3), but verifiers rank an unknown kind 99, so a manifest that lists a new kind beside the existing ones still serves version 1 clients.
15. Reasons and error handling
Conformance is decided by ok, by the step that produced it and by whether a manifest is returned (§7); reasons are diagnostic. The listed implementations return these strings, and the vectors match the substrings shown.
| Step | ok | Manifest returned | Reason |
|---|---|---|---|
| 1 | false | no | no manifest presented |
| 2 | false | no | malformed manifest |
| 3 | false | no | manifest signature does not verify |
| 4 | false | no | unsupported manifest version |
| 4 | false | no | a reason containing requires a newer |
| 5 | false | yes | manifest is for a different service |
| 6 | false | yes | manifest is older than the last-seen (rollback rejected) |
| 7 | true | yes | valid |
select_current returns a manifest or none and reports no reason; a client that wants to log why nothing was selected calls verify_manifest on each candidate. resolve returns an endpoint or none. None of these calls throws.
16. Summary of requirements
Every requirement, by the roles of §1, followed by what each role is permitted to do. The issuer's key custody is listed under the signer, which holds the key.
Signer
- The private issuer key MUST be generated and kept offline, in a ceremony, and MUST be independent of any wallet or funding key. It SHOULD sign nothing except records that carry their own domain separator, or messages under the Bitcoin Signed Message construction (§11).
serviceMUST be non-empty, and every endpointurlMUST be non-empty (§2).- A signer MUST reject a payload that contains a non-integer number anywhere, and MUST reject one that contains an integer whose absolute value exceeds 2^53 - 1 (§2).
- A signer MUST NOT emit any
kindother than the six of §3. It SHOULD include anonionendpoint, and SHOULD warn the operator when it does not (§3). - Member names MUST be ASCII, in this and every later version (§5). Strings SHOULD be printable ASCII (§2).
- A signer MUST produce low-S signatures (§6).
- A signer MUST increase
seqon every change (§8). - Permitted: one issuer key MAY sign for several services (§1).
Publisher
- In the ordinal form, a publisher MUST put the whole token in the single body push after
OP_0(§9). - A publisher that does not know whether its broadcast succeeded MUST NOT simply broadcast again, and MUST report the transaction id it built (§9).
- Permitted: a publisher MAY add the constant marker output beside either form (§9).
Token source
- A candidate that is not valid ASCII MUST be skipped, never accepted, and is never fatal. This binds whoever extracts candidates from transactions, a token source or a client that reads transactions itself (§9).
- The HTTP binding of §13 is stated without keywords. Permitted: a host that serves it MAY add other members to its answer (§13), and a party that extracts candidates MAY accept an ordinal body split across several pushes, concatenating them in order (§9).
Verifier
- A decoder MUST accept the unpadded base64url form (§4).
- A verifier MUST canonicalize the same decoded object that it then reads fields from, and a JavaScript implementation MUST sort keys recursively (§5).
- A verifier MUST ignore members it does not recognise (§2).
- Verification MUST NOT throw or raise, and runs the steps of §7 in order, the signature check before every semantic check (§7).
- Changing any byte of token A's decoded payload, or of its decoded signature, MUST make verification fail (Vector 2).
- Against a floor of 5, a manifest with
seq = 3MUST be rejected and one withseq = 5MUST be accepted (Vector 7). - Permitted: a verifier MAY reject a high-S signature (§6), and MAY reject an edit to a token's text that decodes to the same bytes, such as non-zero unused base64url bits (Vector 2).
Client
- A client MUST NOT treat "found on chain" as evidence of anything (§9).
- A client MUST NOT depend on the marker as its only source of candidates (§9).
- A privacy-sensitive client, for example one in a Tor-only mode, MUST leave
bsvand every clearnet kind out ofallowed_kinds(§10). - An exception thrown by a probe MUST be treated as that endpoint failing, and the caller MUST handle a total failure (§10).
- An
ipendpoint SHOULD be treated as lower assurance (§3). - The issuer key MUST be a constant compiled into the build. Whether a build is a development build MUST be fixed when it is built. A production build MUST NOT honour a stored key, and while a build pin is present a stored value MUST NOT be able to substitute a different key (§11).
- On a conflict between a stored key and the pin, the client MUST refuse discovery, MUST log the refusal as possible tampering, MUST keep its default connection, and MUST NOT go on with the stored key (§11).
- Permitted: a client MAY honour a stored key only in a development build that carries no pin (§11), MAY hard-code the marker script hash (§9), and MAY query the marker at an index that records data-carrier outputs, never as its only source (§§9 and 12).
Later versions
- A later version MAY add payload members, among them a top-level integer
min_reader, and a version 2 manifest MAY add members that a version 1 reader ignores (§§2 and 7). Member names stay ASCII (§5).
Test Vectors
The vectors use two published test keys, fabricated for this document. They must never be used for anything else.
issuer private scalar : 0x11 repeated 32 times
issuer_key : 034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa
other private scalar : 0x22 repeated 32 times
other_key : 02466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27
Every signature below was produced by the reference signer with RFC 6979 nonces and low-S normalization (§6), so a signer built the same way reproduces each DER signature exactly.
Vector 1: sign, canonicalize, verify
Payload: service = "messaging", seq = 5, updated_at = 1700000000, and these endpoints in emit order:
| kind | url | priority |
|---|---|---|
bsv | abababababababababababababababababababababababababababababababab_0 | 100 |
onion | example.onion | 100 |
https | mirror.example.com | 100 |
https | mirror.example.net | 200 |
Wire JSON (compact, emit order):
{"v":1,"service":"messaging","seq":5,"updated_at":1700000000,"endpoints":[{"kind":"bsv","url":"abababababababababababababababababababababababababababababababab_0","priority":100},{"kind":"onion","url":"example.onion","priority":100},{"kind":"https","url":"mirror.example.com","priority":100},{"kind":"https","url":"mirror.example.net","priority":200}]}
Canonical JSON (keys sorted recursively; inside each endpoint the order becomes kind, priority, url):
{"endpoints":[{"kind":"bsv","priority":100,"url":"abababababababababababababababababababababababababababababababab_0"},{"kind":"onion","priority":100,"url":"example.onion"},{"kind":"https","priority":100,"url":"mirror.example.com"},{"kind":"https","priority":200,"url":"mirror.example.net"}],"seq":5,"service":"messaging","updated_at":1700000000,"v":1}
Digest:
SHA256("bsv-reachability-v1" || 0x00 || canonical)
= a43f614861d62aa7eacf3e2bf199297c9ee215c6f826e17f763088890497791d
Token A (base64url(wire_json), a dot, the DER signature in hex):
eyJ2IjoxLCJzZXJ2aWNlIjoibWVzc2FnaW5nIiwic2VxIjo1LCJ1cGRhdGVkX2F0IjoxNzAwMDAwMDAwLCJlbmRwb2ludHMiOlt7ImtpbmQiOiJic3YiLCJ1cmwiOiJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiXzAiLCJwcmlvcml0eSI6MTAwfSx7ImtpbmQiOiJvbmlvbiIsInVybCI6ImV4YW1wbGUub25pb24iLCJwcmlvcml0eSI6MTAwfSx7ImtpbmQiOiJodHRwcyIsInVybCI6Im1pcnJvci5leGFtcGxlLmNvbSIsInByaW9yaXR5IjoxMDB9LHsia2luZCI6Imh0dHBzIiwidXJsIjoibWlycm9yLmV4YW1wbGUubmV0IiwicHJpb3JpdHkiOjIwMH1dfQ.3045022100bacc832b3a9cef05392b4ea7eca4d842be84b3e59130a0ce95a864a2afee8a7c02202eec6b482a900a1d1fa7796c82a2b096333b57309c0b133b2b2c9c8344498dbb
Expected results:
verify_manifest(A, issuer_key)givesok = true, reason"valid",service = "messaging",seq = 5and 4 endpoints.verify_manifest(A, issuer_key, expected_service = "marketplace")givesok = false, reason containing"different service".verify_manifest(A, issuer_key, min_seq = 5)givesok = true. Aseqequal to the floor passes.verify_manifest(A, issuer_key, min_seq = 6)givesok = false, reason containing"rollback".verify_manifest(A, other_key)givesok = false, reason"manifest signature does not verify".verify_manifest(A, "02" + "11" * 32)givesok = false, reason"manifest signature does not verify". That string is not the encoding of a curve point.verify_manifest(x, issuer_key)for eachxinnull,"",".","a.b","no-dot"and"x" * 4000givesok = falseand does not raise.select_current([A], issuer_key, "messaging")gives the manifest withseq = 5.
Ordering:
ordered_endpoints(A)givesbsv abab…ab_0,onion example.onion,https mirror.example.com,https mirror.example.net. The first two share priority 100 and are ordered by rank;mirror.example.netcomes last on priority 200.ordered_endpoints(A, allowed_kinds = {"onion", "https"})drops thebsvendpoint and givesonion example.onion,https mirror.example.com,https mirror.example.net.FallbackResolver(A, {"onion", "https"}).resolve(e => e.url == "mirror.example.net")returns thehttps mirror.example.netendpoint, after the probes ofexample.onionandmirror.example.comreturn false.
Vector 2: one-byte tampering (negative)
Token T1 is token A with one byte of its decoded payload changed: "seq":5 becomes "seq":9, with the signature left as it was. Its wire JSON is:
{"v":1,"service":"messaging","seq":9,"updated_at":1700000000,"endpoints":[{"kind":"bsv","url":"abababababababababababababababababababababababababababababababab_0","priority":100},{"kind":"onion","url":"example.onion","priority":100},{"kind":"https","url":"mirror.example.com","priority":100},{"kind":"https","url":"mirror.example.net","priority":200}]}
and the token is:
eyJ2IjoxLCJzZXJ2aWNlIjoibWVzc2FnaW5nIiwic2VxIjo5LCJ1cGRhdGVkX2F0IjoxNzAwMDAwMDAwLCJlbmRwb2ludHMiOlt7ImtpbmQiOiJic3YiLCJ1cmwiOiJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiYWJhYmFiXzAiLCJwcmlvcml0eSI6MTAwfSx7ImtpbmQiOiJvbmlvbiIsInVybCI6ImV4YW1wbGUub25pb24iLCJwcmlvcml0eSI6MTAwfSx7ImtpbmQiOiJodHRwcyIsInVybCI6Im1pcnJvci5leGFtcGxlLmNvbSIsInByaW9yaXR5IjoxMDB9LHsia2luZCI6Imh0dHBzIiwidXJsIjoibWlycm9yLmV4YW1wbGUubmV0IiwicHJpb3JpdHkiOjIwMH1dfQ.3045022100bacc832b3a9cef05392b4ea7eca4d842be84b3e59130a0ce95a864a2afee8a7c02202eec6b482a900a1d1fa7796c82a2b096333b57309c0b133b2b2c9c8344498dbb
Token T2 is token A with the last byte of its DER signature changed from bb to ba, the payload left as it was.
verify_manifest(T1, issuer_key)givesok = false, reason"manifest signature does not verify".verify_manifest(T2, issuer_key)givesok = false, reason"manifest signature does not verify".
More generally, changing any byte of token A's decoded payload, or of its decoded signature, MUST make verification fail; the rule is stated for token A because its wire JSON has no insignificant whitespace. Some edits to the token text decode to the same bytes, such as the unused low bits of the final base64url character or the case of a hex digit. Those are not forgeries, and a verifier MAY reject them (compatibility note 3).
Vector 3: the highest valid sequence wins
Token B is a genuine manifest at seq = 7, signed by the issuer key. Its wire JSON is:
{"v":1,"service":"messaging","seq":7,"updated_at":1700000600,"endpoints":[{"kind":"onion","url":"example.onion","priority":100},{"kind":"https","url":"mirror.example.org","priority":100},{"kind":"https","url":"mirror.example.com","priority":100}]}
Token B:
eyJ2IjoxLCJzZXJ2aWNlIjoibWVzc2FnaW5nIiwic2VxIjo3LCJ1cGRhdGVkX2F0IjoxNzAwMDAwNjAwLCJlbmRwb2ludHMiOlt7ImtpbmQiOiJvbmlvbiIsInVybCI6ImV4YW1wbGUub25pb24iLCJwcmlvcml0eSI6MTAwfSx7ImtpbmQiOiJodHRwcyIsInVybCI6Im1pcnJvci5leGFtcGxlLm9yZyIsInByaW9yaXR5IjoxMDB9LHsia2luZCI6Imh0dHBzIiwidXJsIjoibWlycm9yLmV4YW1wbGUuY29tIiwicHJpb3JpdHkiOjEwMH1dfQ.3045022100d0db7912eaea0ba4745662540ebd166a836e29d39f0d7353bfa4163e08a341590220659b34996cc50f4dac30a3af8c414c9ebf7ea02e55e7ca1bf8b1af15ad0b8956
Token C claims seq = 9 for the same service and points it at attacker.example. It carries a real signature, made with the other key:
{"v":1,"service":"messaging","seq":9,"updated_at":1700001200,"endpoints":[{"kind":"https","url":"attacker.example","priority":100}]}
Token C:
eyJ2IjoxLCJzZXJ2aWNlIjoibWVzc2FnaW5nIiwic2VxIjo5LCJ1cGRhdGVkX2F0IjoxNzAwMDAxMjAwLCJlbmRwb2ludHMiOlt7ImtpbmQiOiJodHRwcyIsInVybCI6ImF0dGFja2VyLmV4YW1wbGUiLCJwcmlvcml0eSI6MTAwfV19.3044022057c5d7813c9704fc8ca267c3f118665403e5f8e9d7d8625fb2ca96d6235e79d602202f9ff08a4416e241a2c64ec476b81f04dd44b779ea5f70a68f1d7125e00d7d4f
Expected results:
verify_manifest(C, other_key)givesok = true, andverify_manifest(C, issuer_key)givesok = false, reason"manifest signature does not verify".select_current([A, B, C], issuer_key, "messaging")gives the manifest withseq = 7, which is token B. Token C is rejected however high itsseq, and token A loses to B.select_current([A, B, C], issuer_key, "messaging", min_seq = 8)gives none.ordered_endpoints(B)givesonion example.onion,https mirror.example.com,https mirror.example.org. The twohttpsendpoints tie on priority and rank, so they are ordered byurl, against their emit order.
Vector 4: the version rule (§7, step 4)
All three tokens are signed by the issuer key, have service = "messaging" and seq = 6, and list a single endpoint.
Token D has v = 2 and a member this BRC does not define:
{"v":2,"service":"messaging","seq":6,"updated_at":1700000300,"endpoints":[{"kind":"onion","url":"example.onion","priority":100}],"future":["ignored"]}
Token D:
eyJ2IjoyLCJzZXJ2aWNlIjoibWVzc2FnaW5nIiwic2VxIjo2LCJ1cGRhdGVkX2F0IjoxNzAwMDAwMzAwLCJlbmRwb2ludHMiOlt7ImtpbmQiOiJvbmlvbiIsInVybCI6ImV4YW1wbGUub25pb24iLCJwcmlvcml0eSI6MTAwfV0sImZ1dHVyZSI6WyJpZ25vcmVkIl19.30440220026f1dd794a25cbf8404c10338e8ca97ad531b50a41b5ac3ff91963f2e0a8aa902207293236df0f25967ca5731a5b1bd95e7f48837bd9d555fe4befbed40b67ac6f3
Token E has v = 2 and min_reader = 2:
{"v":2,"service":"messaging","seq":6,"updated_at":1700000300,"endpoints":[{"kind":"onion","url":"example.onion","priority":100}],"min_reader":2}
Token E:
eyJ2IjoyLCJzZXJ2aWNlIjoibWVzc2FnaW5nIiwic2VxIjo2LCJ1cGRhdGVkX2F0IjoxNzAwMDAwMzAwLCJlbmRwb2ludHMiOlt7ImtpbmQiOiJvbmlvbiIsInVybCI6ImV4YW1wbGUub25pb24iLCJwcmlvcml0eSI6MTAwfV0sIm1pbl9yZWFkZXIiOjJ9.3045022100bab2863b50c5870fb8830b6aa193d135a956b3a3e0370b0ef899ecea287be22302202dd34f27d88913f080c15ef49d4c9b780ae7391a13d809e237f1e67fa7741e41
Token F has v = 0:
{"v":0,"service":"messaging","seq":6,"updated_at":1700000300,"endpoints":[{"kind":"onion","url":"example.onion","priority":100}]}
Token F:
eyJ2IjowLCJzZXJ2aWNlIjoibWVzc2FnaW5nIiwic2VxIjo2LCJ1cGRhdGVkX2F0IjoxNzAwMDAwMzAwLCJlbmRwb2ludHMiOlt7ImtpbmQiOiJvbmlvbiIsInVybCI6ImV4YW1wbGUub25pb24iLCJwcmlvcml0eSI6MTAwfV19.304402200b617d1280afc72f0b4a489121a901e8f53207ca306f6e4d83d28bc319b606b302205f71cf0576b8a96406896ac2bf833f26f92ca030c085e700960e3ecbf92c70eb
A version 1 verifier gives:
verify_manifest(D, issuer_key, expected_service = "messaging"):ok = true,seq = 6, one endpoint. The unknown member is ignored, and it is still covered by the signature.verify_manifest(E, issuer_key, expected_service = "messaging"):ok = false, reason containing"requires a newer".verify_manifest(F, issuer_key, expected_service = "messaging"):ok = false, reason"unsupported manifest version".
Vector 5: on-chain scripts (§9)
marker_script = OP_FALSE OP_RETURN "TKRMREG1" = 006a08544b524d52454731
marker_script_hash = SHA256(marker_script), reversed, hex
= 6f33820d5cb3f85c23bdd46daeea10bc197c3ef2c93f9a51b6e2e5bdedab49ce
The carrier output for token A is 624 bytes. It is these 11 bytes followed by the 613 ASCII bytes of token A:
006a 05 544b524d31 4d 6502
That is OP_FALSE OP_RETURN, a 5-byte push of TKRM1, then PUSHDATA2 with length 0x0265 (613).
The ordinal output for token A, locked to a P2PKH whose 20-byte hash is 0x33 repeated, is 661 bytes, with the whole token in one push:
76a914 3333333333333333333333333333333333333333 88ac P2PKH lock
00 63 03 6f7264 51 0a 746578742f706c61696e 00 OP_FALSE OP_IF "ord" OP_1 "text/plain" OP_0
4d 6502 <the 613 bytes of token A> PUSHDATA2, 613 bytes
68 OP_ENDIF
Reading either output back as in §9 recovers token A exactly.
Vector 6: build-pin resolution (§11)
P is the pinned build key, S the stored key and dev_build the build-time flag. K is the issuer key above and K2 is the other key.
Pinned P | Stored S | dev_build | Result key | conflict | Meaning |
|---|---|---|---|---|---|
K | "" | false | K | false | The build pin is authoritative. |
K | K in upper-case hex | false | K | false | Case-insensitive match, accepted; the pinned value is returned. |
K | K2 | false | "" | true | The stored key tried to replace the pin. Refused; discovery stays inert on the default connection. |
K | K2 | true | "" | true | A development build cannot relax a pin either. |
"" | K | true | K | false | Development build with no pin: the stored override is allowed. |
"" | K | false | "" | false | Production build with no pin: the stored key is ignored and discovery is inert. |
"" | "" | false | "" | false | Nothing pinned, nothing stored: discovery is inert (fail-closed). |
Vector 7: the sequence floor (§8)
update_floor(current, seq) = max(current, seq), with an unset floor treated as lower than every seq:
| Current floor | Incoming seq | New floor |
|---|---|---|
| unset | 5 | 5 |
| 5 | 3 | 5 (an older manifest never lowers the floor) |
| 5 | 5 | 5 (the same manifest read again) |
| 5 | 9 | 9 |
Against a floor of 5, a manifest with seq = 3 MUST be rejected with a reason containing "rollback", and one with seq = 5 MUST be accepted.
Implementations and compatibility notes
Four implementations exist. None is public yet, so they are described here rather than linked.
- A Python reference signer, verifier and publisher on bitcoinx over libsecp256k1, with RFC 6979 nonces and low-S signatures, refusing high-S ones; it produced every vector signature. Its publisher builds the outputs of §9, reads candidates back out of a transaction, and stops with the transaction id it built when a broadcast outcome is unknown. Its mint tool signs a token for an external broadcaster without a wallet or the network, reading the issuer key only from a key file or a hidden prompt.
- Two TypeScript readers, one on
@noble/curves, which refuses high-S signatures, and one on@bsv/sdk, which accepts them. Each sits in an application with the §11 resolver behind a build-time development flag, a persisted floor and a token-source reader, and both ship the pin empty. The@noble/curvesapplication runs discovery at start-up and mapsonion,httpsandnostrendpoints to relay URLs. The@bsv/sdkapplication probesonionandhttpsendpoints, and its start-up hook is present but not yet called. - A relay node in JavaScript, which wraps a token minted elsewhere in the carrier and marker of §9, byte for byte as the reference does, broadcasts it under its own funding key, and records it once handed to the network, without learning whether it was mined. It serves what it has published at
GET /reachability, unverified, deduplicated, oldest first and at most 50, exactly as received, withAccess-Control-Allow-Origin: *and the marker script hash.
Vectors 1 to 4 pass in all three readers' tests, Vector 5 in the Python tests and Vector 6 in both applications' resolver tests. A separate program on the Python standard library and OpenSSL checked every vector value as printed, an independent RFC 6979 signer reproduces the Vector 1 signature, and on 20,000 random payloads within §2 the reference's canonical bytes equalled the recursive-sort serialization of the JavaScript readers. The reader behaviour in the notes below was observed by running the three readers on the same tokens. No manifest has yet been published on chain.
Compatibility notes
-
Wrongly typed members (§2). All three readers refuse at step 2 a payload whose
service,kindorurlis not a string, whoseseq,updated_ator presentpriorityis not an integer within ±(2^53 - 1), or whoseendpointsis not an array of objects, including a signednullrewritten as1e400(Claim 1). Avormin_readerthat is not such an integer is refused at step 2, 3 or 4, depending on the reader and the spelling. -
Float spellings and unread members (§§2 and 5). A signed integer written on the wire as
5.0or5e0is refused by the reference, whose decoder returns a float, and read as the integer by the JavaScript readers. In a member no reader reads, the reference accepts an integer beyond the bound and refuses any float, and the JavaScript readers accept any value whose canonical form reproduces the signed bytes,1e400for a signednullincluded, and refuse an integer a double cannot hold, such as 2^53 + 1. -
Encodings of one token (§§4 and 6). None of these is a forgery: each decodes to the signed bytes or fails.
Variation of a genuine token Reference @noble/curves@bsv/sdkHigh-S twin of the signature refuses refuses accepts DER rwithout its0x00sign byterefuses refuses accepts Other malformed DER (padding, length, trailing byte, tag) refuses refuses refuses Whitespace inside or after the signature hex accepts refuses refuses Whitespace before or inside the base64url part refuses refuses accepts The standard base64 alphabet accepts refuses accepts Non-zero unused bits in the last base64url character accepts refuses accepts Wire JSON in UTF-16 accepts refuses refuses An integer written 5.0or5e0(note 2)refuses accepts accepts A signed nullwritten1e400in an unread member (note 2)refuses accepts accepts All three accept upper-case signature hex, restored
=padding, a UTF-8 byte order mark, and wire JSON re-spelled by whitespace, member order,\uescapes or a repeated member ending in the signed value. A source that serves only a variant some readers refuse withholds the manifest from them; the relay node serves tokens as received, so one stored with a trailing line ending verifies only in the reference. -
Non-ASCII names and strings (§§2, 5 and 10). With member names U+FFFF and U+1F600 the reference verifies and the readers, which sort by UTF-16 code unit, fail. A signed lone surrogate verifies only in the readers, and locators that differ above U+FFFF tie-break differently.
-
Locators and kinds (§§3 and 10). No reader checks a locator against its kind, and all three rank 99 a kind named after an inherited JavaScript property, such as
constructor. The@bsv/sdkapplication keeps an explicithttp://orhttps://scheme, so anonionendpoint written as a clearnet URL is fetched over the clearnet and anhttpsendpoint writtenhttp://without TLS. The@noble/curvesapplication keeps an explicitws://orwss://scheme, and in its Tor-only mode refuses every host that is not an onion, whatever the label. -
The floor and the token source (§§8 and 12). The
@noble/curvesapplication raises its floor at selection, the@bsv/sdkapplication only after an endpoint answers (Claim 6). Each keeps one floor, not keyed by issuer key, in browser storage with no integrity check, beside the token-source URL, the Tor-only setting and an opt-out flag; after a key rotation the new key's manifests are refused below the old floor. Both refuse to record aseqthat is not an integer within the bound and treat such a stored floor as none, logging an error, and in a Tor-only mode both refuse a token source whose parsed host is not an onion. -
Probing and
bsv(§§10 and 12). On total failure both applications keep their default connection without telling the user. The@noble/curvesapplication does not probe; it takes the first relay URL. The@bsv/sdkapplication has an unchecked path for fetchingbsvcontent through a clearnet gateway, which never runs because its only caller excludesbsv. -
Reading transactions (§9). The reference, the only listed parser of transactions, reads a carrier only when it begins
00 6a, accepts every push encoding, stops at the first opcode that is not a push, takes only the push after the tag, and does not search a carrier for an inscription. It reads the first inscription envelope anywhere in the script, matchingtext/plainas a case-sensitive prefix. -
Signer checks (§§2, 3 and 8). The reference's builder and signing call both refuse every payload §§2 and 3 forbid a signer to write, and a negative
seq, truncating nothing; the builder warns when noonionendpoint is listed. Nothing refuses a reused or lowerseq. -
Reason strings (§15). The reference words its
min_readerreason differently from §15, but it containsrequires a newer.
Design rationale and alternatives considered
- The chain, because a mined output stays in every unpruned copy of its block without anyone's cooperation, where an IPNS record lapses unless republished and a Nostr relay keeps what it chooses.
- A pinned key, because a key fetched at run time or published at a domain arrives through the channel a takedown attacks.
seq, rather than a timestamp, because an offline signer may lack a trustworthy clock, and time ordering lets one future-dated record hold off every later one. 2^53 values last about 285 million years at one per second.- An equal
seqpasses the floor, so that a restart is not mistaken for a rollback. - A bare ECDSA signature, because Bitcoin Signed Message shares one prefix among all its applications and wallets sign user-supplied text under it on request, while BRC-3 and BRC-77 would put BRC-42 key derivation into every reader and the offline ceremony.
- This canonical JSON, one standard-library call in Python and a short function in JavaScript (Claim 2); BRC-192's stricter rules would change what existing readers accept without changing what an honest signer writes.
- Closed kinds with fixed ranks, so that the try-order is a function of the manifest and encodes each kind's takedown posture.
- No expiry, because with an offline key, expiry would let anyone who can prevent the next publication end discovery for every client.
- Two on-chain forms, because pruning nodes and indexes of spendable outputs drop the carrier, while the ordinal stays in every node's unspent set until it is spent.
- No overlay topic, because a topic manager runs on an overlay host, which the client cannot yet reach; a federated topic could later be one token source among several.
- One issuer key, for one verification per candidate and the simplest ceremony; a threshold would need a new payload version and signing domain (§14).
- A floor kept by the client, one integer, where order on chain would need proofs of inclusion against a chain of headers.
Mathematical and cryptographic basis
|| is concatenation and |X| a length in bytes. n and G are the order and base point of secp256k1 (SEC 2), d the issuer private scalar, Q = d*G, H SHA-256 and D the 19 bytes bsv-reachability-v1. C is canonical of §5 and E(P) = D || 0x00 || C(P); C_W and E_W are the same computed by a reader W from the value its decoder returns. ECDSA verification of (r, s) over a digest e under Q (SEC 1, §4.1.4) checks 1 <= r, s < n, computes R = (e * s^-1)*G + (r * s^-1)*Q, and accepts if R is not the point at infinity and its x-coordinate modulo n is r. The payload space is:
V = null | true | false | Int | Str | Arr | Obj
Int = the integers i with -(2^53 - 1) <= i <= 2^53 - 1
Str = finite sequences of Unicode scalar values (U+0000 to U+D7FF and U+E000 to U+10FFFF)
Arr = finite sequences of elements of V
Obj = finite maps from member names to elements of V, where a member name is an
element of Str whose characters are all below U+0080
A payload is an element of Obj with the members of §2; the bounds come from §2, §5 and the reference signer's UTF-8 step, which refuses a lone surrogate. All the listed verifiers resolve a member repeated on the wire to its last occurrence.
Claim 1: the encoding is injective
Claim 1. C is injective on V, and so is E.
Proof sketch. A JSON parser (RFC 8259) that returns integers exactly and strings as scalar values inverts C, by induction on P. Literals are written as themselves. An integer in decimal without fraction or exponent is read back exactly, in JavaScript too, since every element of Int is a distinct double. A string's escapes and UTF-8 are uniquely decodable, with no unescaped " inside. Arrays and objects are recovered element by element, and a map has distinct names. The fixed prefix D || 0x00 keeps the encoding injective. ∎
Sorting, the fixed separators, the ASCII names and the integer bound play no part in the proof; they make implementations agree on C. Each reader's C_W is injective with two exceptions, both in the JavaScript readers: a -0 encodes as 0, which nothing in §§7, 8 or 10 distinguishes, and a number too large for a double, such as 1e400, decodes to an infinity, which JSON.stringify writes as null. Anyone can therefore write 1e400 where the issuer signed null and the signature still verifies, but the listed readers refuse an infinity, as they refuse null, in every member they read (compatibility note 1). Security does not depend on the rules that define V: a payload outside V can only make readers disagree on whether a genuine token verifies.
Claim 2: the implementations compute the same encoding
Claim 2. On V, the Python rule of §5 and the recursive sort of the TypeScript readers produce the same bytes, equal to the output of the RFC 8785 serialization steps (its §3.2), and to the RFC 8785 canonical form when no string holds a Unicode noncharacter.
Argument. Separators, literals and string escapes coincide, and every other scalar value, U+007F, U+2028 and U+2029 included, is written as UTF-8. RFC 8785 writes numbers as Number.prototype.toString, which gives every element of Int as plain digits, as Python does. RFC 8785 and the readers sort names by UTF-16 code unit and Python by code point, which agree on ASCII. RFC 8785 requires I-JSON input, which forbids noncharacters (RFC 7493 §2.1); Str admits them, and §5 writes them as the RFC 8785 serialization steps would. Outside V the rules part on floats (5.0, 1e+16), on integers beyond 2^53, on lone surrogates, which Python and RFC 8785 refuse and the readers escape, and on the infinities, which the readers write as null.
Claim 3: unforgeability
The adversary knows Q, may obtain issuer tokens for payloads of its choice, controls every token source and may submit any string. It wins if a conforming reader W accepts a token whose signed bytes E_W(P*) differ from E(P_i) for every payload P_i the issuer signed. The assumption is that ECDSA over secp256k1, signing M by signing H(M), is existentially unforgeable under an adaptive chosen-message attack (EUF-CMA).
Claim 3. For every such adversary A that obtains q issuer tokens, there is an adversary B against ECDSA with SHA-256 that makes q signing queries, runs in the time of A plus q + 1 canonical encodings, and forges with probability at least Pr[A wins].
Proof. B runs A on Q, answering each query P_i by asking its oracle to sign M_i = E(P_i). When A outputs a token that W accepts, B decodes it as W does into P* and sigma* and outputs (E_W(P*), sigma*). W accepted at step 3 of §7, so sigma* verifies over H(E_W(P*)), and A won, so E_W(P*) is no M_i. ∎
The reduction is tight, and a SHA-256 collision, or two digests congruent modulo n, is already a forgery for B. The build pin (§11) is what fixes Q in this game.
Strength of the assumption. EUF-CMA security of ECDSA is proved only in idealized models (Brown, 2005; Fersch, Kiltz and Poettering, 2016), and the best known attacks are generic: Pollard's rho needs about 2^127 group operations, a SHA-256 collision about 2^128 evaluations, and two uniform 256-bit digests are congruent modulo n without being equal with probability about 2^-382.7. An attacker with 2^80 operations succeeds with probability below about 2^-94. A quantum computer running Shor's algorithm at this size recovers d from the public Q, and this BRC claims nothing against one.
Claim 4: one token, one meaning
Claim 4. (1) If two conforming readers both accept the same token, they verified the same signed bytes, unless one accepted a forgery in the sense of Claim 3. (2) If a reader accepts a token whose signed bytes are E(P), where every member of P that this BRC defines has the type §2 gives it, as in every payload a conforming signer produces, it reads the manifest it reads from the issuer's own token for P, except that a -0 may stand for a 0.
Proof. (1) The three DER parsers read the same (r, s) from every encoding more than one of them accepts. Suppose the readers verified different messages with digests e1 and e2 modulo n. Both were issued, or one reader accepted a forgery (Claim 3), and the token's (r, s) is the issuer's signature over one of them, say the first, or its twin. Outside a fraction of about 2^-127 of signatures, (r, s) verifies over exactly two digests, e1 and e1' = -e1 - 2*r*d mod n. If e2 = e1, two issued messages have congruent digests (Claim 3). If e2 = e1', then since d = -(e1 + e1') / (2r) mod n, a payload chosen without d lands there with probability about 1/n. (2) The decoded value P* has C_W(P*) = C(P). C_W is injective except that -0 shares the encoding of 0 and the infinities that of null (Claim 1), and null occurs in C(P) only where P holds it, never in a defined member, so P* agrees with P on every member the reader reads, up to -0. ∎
The listed readers refuse every token in which a member they read lacks its §2 type (compatibility note 1), so for them part (2) holds without its condition. A token's text does not identify a manifest, since anyone can derive many accepted variants of one genuine token (compatibility note 3), the high-S twin (r, n - s) among them. Strong unforgeability is not needed, because every check after step 3, selection and the endpoint order depend only on the decoded payload; deduplicating candidates by canonical bytes bounds them, and deduplicating by token text does not.
Claim 5: domain separation
Claim 5. If D1 and D2 are distinct non-empty byte strings that contain no 0x00, no byte string begins with both D1 || 0x00 and D2 || 0x00.
Proof. If |D1| = |D2|, the prefixes differ within their first |D1| bytes. If |D1| < |D2|, byte |D1| (counting from zero) is 0x00 in the first and a byte of D2, which is not 0x00, in the second. The case |D1| > |D2| is symmetric. ∎
A protocol that signs under its own such tag therefore has messages disjoint from this BRC's, and a signature valid in both needs congruent digests or the second digest of Claim 4. Transactions and Bitcoin Signed Message sign H(z) for a 32-byte z, while every object that passes step 2 in the listed verifiers contains endpoints, so |E(P)| >= 36 and the same holds. A BRC-77 signature is made under a child key Q + h*G and verifies under Q only if h is congruent to 0 modulo n. The separator does not stop a signing oracle, which is why §11 confines the issuer key to tagged records, nor separate services under one key, which the signed service and step 5 do. The signed bytes name no network or output, so a copied token is the same manifest.
Nonces
ECDSA sets r to the x-coordinate of k*G modulo n and s = k^-1 * (e + r*d) mod n for a nonce k that must be secret, uniform and never reused. Two signatures that share r over digests that differ modulo n give up d whatever low-S normalization did, since an attacker can solve k = (e1 - e2) / (s1 - eps*s2) mod n for eps equal to 1 and -1 and keep the d = (s1*k - e1) / r mod n with d*G = Q; a few biased nonce bits across many signatures let lattice reduction do the same. The reference derives k by RFC 6979 before normalizing to low-S, so signing is deterministic, its nonces are pseudorandom when HMAC-SHA-256 is modelled as a random oracle, and signing needs no random number generator, though generating d does. RFC 6979 is not required. Deterministic signing is exposed to fault injection, and the reference does not verify its own output, so an issuer on untrusted hardware can verify each token under Q before publishing it.
Claim 6: rollback
G_S is the set of payloads the issuer has signed for a service S, each with the members and types of §2, and the issuer discipline is that distinct payloads in G_S have distinct seq values (§8). The floor F is an integer or unset, and unset is below every integer. In round t the adversary supplies any finite list of strings, and A_t is the set of payloads decoded from them that pass verify_manifest with expected_service = S and min_seq = F_(t-1). If A_t is not empty the client selects the element m_t with the greatest seq and sets F_t = max(F_(t-1), seq(m_t)); otherwise F_t = F_(t-1).
Claim 6. Unless a forgery occurs (Claim 3), in every round:
- every selected
m_tis inG_S; F_t >= F_(t-1), andseq(m_t) >= F_(t-1);- if rounds
t < t'both select, thenseq(m_t') >= seq(m_t); - if the list in round
tcontains an accepted encoding of someginG_Swithseq(g) >= F_(t-1), thenseq(m_t) >= seq(g), and under the issuer disciplinem_tisgor a later genuine manifest; - under the issuer discipline, a manifest whose
seqequals the floor is the manifest that set the floor.
Proof. (1) follows from step 3 of §7, Claims 3 and 4, and step 5. (2) follows from the definition of F_t and from step 6, since by Claim 4 the seq read from a genuine payload is the integer signed. (3) seq(m_t') >= F_(t'-1) >= F_t >= seq(m_t), by (2). (4) holds because g is in A_t. (5) The floor equals seq(m_u) for the round u that last raised it, and under the discipline only m_u has that seq. ∎
So the adversary can choose, in each round, any genuine manifest for S at or above the floor, or prevent selection, and nothing more. The claim rests on four conditions. The floor is raised on selection (§12, step 4); a client that raises it only after an endpoint answers gets (3) only for rounds in which it connected. The issuer discipline holds; otherwise two genuine payloads tie, and the order of the adversary's list decides. The floor survives in client storage; a party that can write storage can lower it, though it cannot introduce a forgery or, by §11, replace the key. Every genuine payload has the types of §2; a reader that read a wrongly typed seq loosely could break (2) and (3), as JavaScript reads a non-numeric seq as NaN, which passes a < test against any floor, and a signed null rewritten as 1e400 as an infinity, which wins selection. The listed readers refuse both.
None of the claims concerns freshness, control of endpoints, recovery from key theft, confidentiality or quantum attack.
Security Considerations
What a verified manifest proves
A manifest that passes §7 and wins selection under §8 proves that the holder of the pinned issuer key signed this list of endpoints, for this service, at this sequence number. It does not show that the manifest is the newest (a source can withhold, and a fresh install accepts any genuine manifest ever signed), that it is the only one at its seq, that it was ever published on chain, that any endpoint is honest, still the service's, or reachable, or, for a payload no conforming signer produces, what its values mean. Discovery therefore also rests on the build that carries the pin (§11), on the token sources (§13), and on the kind of locator each endpoint uses.
A self-certifying locator commits to a key or to content: an onion address encodes the service's key, a bsv inscription id names a transaction whose hash commits to the bytes, and an IPFS identifier hashes its content. A delegated locator, such as an https name, an ip address or a relay's domain, is assigned by a third party and can change hands while a manifest that lists it stays valid; bsv and ipfs become delegated to any gateway the client lets check the content. Every attack that makes a client act on an older list does its harm through delegated locators, or through self-certifying ones whose keys have been taken.
Adversaries
| Adversary | Can | Mitigation | Residual risk |
|---|---|---|---|
| Registrar or DNS provider | suspend (RFC 5731 serverHold, clientHold), transfer or redirect names and obtain certificates for them, which DNSSEC does not prevent | the pin never passes through DNS; onion and bsv have no registry; a new manifest without the name | a client shown an older manifest reaches the wrong party over valid TLS; a web client loaded from a seized origin |
| Host or cloud provider | stop, image or impersonate hosted endpoints with their TLS and onion keys; run a lying token source | the issuer key is offline (§11); a new manifest, with a new onion address, since none can be revoked; endpoints across providers | seized endpoints, until a client sees the new manifest |
| App store or other code channel | delist the app, block updates, ship its own build if it holds the signing key | installed builds keep their pin; the bsv kind; other channels | a blocked rotation build; a replaced pin; trust on first install |
| Network observer or censor | see connections; block listed endpoints, the token source or Tor | onion over Tor with a pluggable transport; mirrors handed out by other means | listed mirrors; Tor itself blocked |
| Malicious or lagging token source | withhold, freeze, choose among genuine manifests, serve variants, flood, log clients | verification; the floor; selection over several sources, one honest and current | staleness for a client with one source, as in each listed application |
| Chain spammer | publish junk and replayed tokens without limit | verification never raises; the marker is optional (§9) | an index that caps or truncates the marker's history can return nothing usable, or omit the newest manifest |
| Miner | decline to mine a publication | other miners; tokens served off chain stay valid | miners acting together |
| Ordinals indexer or gateway | withhold inscriptions; serve other bytes for bsv; serve all inscriptions from one shared web origin | a client that checks the transaction; an origin of the shell's own | a client that runs unchecked bytes (no listed one fetches bsv) |
| Issuer-key thief | anything a manifest can express, for every client | offline custody (§11); monitoring the chain | full trust in the stolen key until a new build arrives |
| Insider at the service | sign, if at the ceremony; republish, withhold or serve selectively | separate funding and signing keys; a signing record | an insider with the key is a thief |
| Client-storage attacker | rewrite the floor, token source, privacy mode and opt-out flag | §11 stops it substituting the key | rollback, freezing and loss of Tor-only mode |
Issuer-key thief. A compromised issuer key is a total compromise. A thief can list endpoints it controls at any priority, an onion address included; put a clearnet locator under the onion label (Locator confusion); serve its own application through bsv, which makes the issuer key a code-signing key; list a victim's URL, or loopback and private addresses, for every client to probe; exhaust the floor; sign a second manifest at the current seq, which moves clients without raising any floor (Equivocation); sign wrongly typed members, which a loosely typed reader would let past its floor; and serve tokens privately, leaving no public trace. An issuer that records every token it signs can watch the chain for others that verify under its key, as a domain owner watches Certificate Transparency, but that catches only a thief who publishes, and floors the thief raised stay raised after rotation. A quantum computer running Shor's algorithm would make anyone a thief; a new domain string (§14) gives a later version a path to another signature scheme.
Client-storage attacker. An information stealer, an injected script, or any inscription sharing a gateway origin with the shell can write client storage, and relaxing §11 so that a stored key overrides the pin is a critical vulnerability. The listed applications keep the other settings in browser storage with no integrity check (compatibility note 6), so an attacker can lower the floor, opening rollback to any genuine manifest; raise it past any future seq; point the client at its own token source, which freezes manifests and logs the client's address long after the foothold is gone; or turn Tor-only mode or discovery off.
Freshness and the freeze attack
A freeze attack, in TUF's terms, withholds newer metadata without presenting anything invalid. Here it needs only control of a client's token sources, or one lagging source, and the floor does not help. If endpoints have only died, the client falls back to its default connection; if a delegated locator in the stale list has changed hands, or a self-certifying one has lost its key, the client connects to the new holder. There is no expiry (Design rationale), and updated_at, which no listed implementation reads, is the offline signer's own claim; a later freshness rule would need a time the client can trust, such as the mining block's. A publisher limits the damage by preferring self-certifying kinds and by keeping control, for good, of every delegated locator any acceptable manifest has listed.
Equivocation
Two different valid manifests at one seq come from an operator's mistake, which §8 forbids, or from a key thief. The listed implementations keep whichever they meet first, and a client whose floor equals that seq accepts either, so a thief can move clients between them without raising any floor. A deterministic rule such as Nostr's lowest id would make clients agree, though a key holder can grind payloads until its own wins, and treating two manifests at the highest seq as evidence of compromise would defeat the attack. This version does neither.
Floor exhaustion and recovery
A signed seq cannot be withdrawn, so one signed in error, such as a test manifest under the production key, moves the floor of every client that reads it, permanently. A thief who signs at seq = 2^53 - 1 leaves the issuer able to publish only at that seq, with selection following discovery order: TUF's fast-forward attack. The listed readers refuse a larger seq (compatibility note 1), though §2 binds only signers. A rotation build stops the thief's manifests verifying, but a floor stored without the key it was earned under, as in the listed applications, refuses the new key's manifests below it, and this BRC does not specify how a new build resets it.
Key rotation, revocation and compromise recovery
The pin changes only with a new build (§11), the price of keeping runtime state, DNS and manifests from moving the trust root. There is no revocation, so a client trusts a stolen key until it installs a build without it; recovery has no time bound if the channel that would deliver that build is under attack; and a key that signs for several services (§1) exposes all of them. DNSSEC's in-band rollover (RFC 5011), TUF's chained root metadata and KERI's pre-rotation each move a trust root under rules fixed beforehand, and each could be added in a later version without weakening the pin.
Downgrade of endpoint kinds
A party that makes preferred endpoints fail, for example by blocking Tor, moves a client down the order of §10 onto delegated and clearnet kinds if its allowed_kinds admits them. Failures never add a kind, which is why §10 requires a privacy-sensitive client to keep to onion. Two downgrades lie outside allowed_kinds: the publisher sets priority and can place https ahead of onion, and allowed_kinds filters on the signed label, so it is only as strong as the client's check that each locator matches its label.
Parser differentials
Because a verifier canonicalizes the very object it then reads (§5), a parser differential can make verifiers disagree about validity, and cannot, without the key, make them accept different signed bytes or, for a payload a conforming signer produces, different contents (Claim 4). Each differential in compatibility notes 1 to 4 changes only whether a token is accepted, except the tie-break of note 4, which changes only the order in which endpoints are tried.
Resource exhaustion
Everything before step 3 of §7 runs on unauthenticated input, and this version bounds none of it; the listed readers set no timeout or size limit on the fetch of §13. In testing every reader refused a 20 MB token, a 5,000-digit number and 200,000 levels of nesting cleanly, but a well-formed junk candidate costs one verification, 2 to 3 ms in each JavaScript reader, and a manifest with thousands of endpoints can keep a resolver probing for hours. A source can withhold anyway, so a client loses nothing by bounding response size and duration, token length, candidates verified and endpoints probed.
Locator confusion
The issuer signs each locator, so confusion is first a hazard at signing time, which the printable-ASCII recommendation of §2 addresses by forcing an internationalized name into its xn-- form. Each kind has confusions of its own:
| Kind | Confusions | The check that resolves them |
|---|---|---|
onion | a vanity address sharing a prefix with the real one; .onion in a clearnet path or label, as in a.onion.example.com; a clearnet URL under the onion label | compare all 56 characters; take the host from a parsed URL; require a version 3 address |
https | an explicit http:// or ws:// scheme; user information, as in mirror.example.com@attacker.example; a numeric host that parsers rewrite, as 2130706433 becomes 127.0.0.1 | parse the URL; require a TLS scheme; refuse user information and loopback, link-local and private addresses |
ip | the same numeric forms; no key bound to the endpoint | treat the endpoint as lower assurance (§3) |
bsv | a bare txid, which leaves the output to the gateway; another ordinal scheme's i separator; a txid's display byte order | use <txid>_<vout>, then fetch and check the transaction |
ipfs | a mutable /ipns/ or DNSLink name | require an immutable content identifier |
nostr | a public-key, profile or NIP-05 identifier where a relay URL is meant; ws:// without TLS | require a wss:// relay URL |
This version requires none of these checks, and the listed applications differ in which they apply (compatibility note 5).
Summary of residual risks in this version
These limitations are stated so that a deployment can plan for them; none is a requirement.
- No manifest expires, and nothing bounds how stale a client's view can be (Freshness and the freeze attack).
- There is no revocation, successor key or threshold of keys (Key rotation, revocation and compromise recovery).
- The floor is not bound to the issuer key, and nothing resets it on rotation (Floor exhaustion and recovery).
- Two manifests at one
seqare resolved by discovery order (Equivocation). - A verifier is not required to check the types of a correctly signed payload, and one that converted them loosely could be steered past its floor (compatibility note 1).
- Nothing bounds what a client accepts from a source (Resource exhaustion).
- Locator syntax is unchecked, and nothing ties a locator to its kind (Locator confusion).
- A client does not learn whether a token was ever published, and each listed application reads one token source.
Privacy Considerations
A manifest provides authenticity and integrity, and no confidentiality: its endpoints are meant to be found. What matters is who learns what when a client uses one, and what an operator reveals by publishing one.
| A client's contact | Who learns its network address | What else they learn |
|---|---|---|
| A token source over the clearnet | the operator and the network path | that the client uses discovery, and perhaps for which service |
| A token source that is an onion service | nobody on the path | that someone uses discovery |
| A marker lookup at an index | the index operator | that the client seeks manifests, and from its next fetches for which issuers |
A bsv or ipfs endpoint | the gateway operator, or for native IPFS the peers asked for providers | an identifier that names the service |
An onion endpoint | nobody, given Tor | |
An https endpoint | the server, the DNS resolver and the path | the host name, unless Encrypted Client Hello hides it |
An ip or nostr endpoint | the server or relay and the path | the service's address, or the client's subscriptions |
Every endpoint probed learns of the client, including those that fail. The token source is contacted first, so a Tor-only client that fetches tokens over the clearnet gains nothing from an onion-only allowed_kinds; a source's onion status is judged from the parsed host, since a clearnet URL can carry .onion in its path.
Publishing on chain publishes for good. The sequence of manifests is a public, dated map of every mirror, address and onion service the service has listed, so a home address or staging host has no place in one, and an onion address listed beside a clearnet endpoint on the same machine reveals where the onion service runs. The issuer key ships in every build and can be tested against every manifest, so services that share a key are publicly linked; the funding transaction ties the publisher's coins to the service; and updated_at and mining times show when the operator works.
Deployment and operational guidance
These practices follow from the analysis above and are not requirements.
- The ceremony. Generate the issuer key on a machine that never connects to a network, sign there, and carry out only the finished token. Pass the key to the signing tool through a file or a prompt, never on a command line or in an environment variable, where shell history and process listings keep it, as the listed mint tool requires. Keep a record of every token signed, apart from the key.
- Sequence numbers. Take each new
seqas the next integer after the last one signed for the service, check it against the record, and sign test manifests with a test key. - Endpoints. Prefer self-certifying kinds, include an
onionendpoint, spread clearnet endpoints across providers, and keep every listed domain registered indefinitely. - Publication. Republish when an endpoint changes, use more than one token source, and strip line endings from a token before publishing it.
- Monitoring. Watch the chain for any token that verifies under the issuer key and is missing from the signing record, and for two manifests at one
seq. - Clients and rotation. Ship the pin empty until the real key exists, use an onion token source in a Tor-only mode, and raise the floor at selection (§12). Plan a rotation build, including how it treats a floor raised under the old key, before it is needed.
Backwards compatibility
No manifest has been published on chain under this or any earlier form of the scheme, and the listed implementations ship with discovery inert, so this version has no predecessor to stay compatible with. Its forward compatibility rests on §§7 and 14: a version 1 reader accepts later additive versions, and a publisher marks a breaking one with min_reader.
References
- BRC-3: Digital Signature Creation and Verification.
- BRC-22: Overlay Network Data Synchronization.
- BRC-24: Overlay Network Lookup Services.
- BRC-42: BSV Key Derivation Scheme (BKDS).
- BRC-43: Security Levels, Protocol IDs, Key IDs and Counterparties.
- BRC-68: Publishing Trust Anchor Details at an Internet Domain.
- BRC-77: Message Signature Creation and Verification.
- BRC-88: Overlay Services Synchronization Architecture (SHIP, the Services Host Interconnect Protocol, and SLAP, the Services Lookup Availability Protocol).
- BRC-101: Diverse Facilitators and URL Protocols for SHIP and SLAP Overlay Advertisements.
- BRC-169: Universal Handle Addressing and Resolution for the Metanet.
- BRC-178: Race-Settled Collection Markets for Overlay Lookups and Message Boxes (its section on bootstrapping and default SLAP trackers).
- BRC-179: Signed Capability Manifest for Wallet and Tool-Server Method Exposure (section 9, the build pin and its limit).
- BRC-180: Overlay Service Discovery at an Internet Domain.
- BRC-183: A Framework for Strict and Federated Overlays.
- BRC-192: Application Output Knowledge and Source Adapters (section 1, shared representation rules).
- BRC-194: Overlay Capabilities and Non-Final Proposal Sessions.
- RFC 2119: Key words for use in RFCs to Indicate Requirement Levels, and RFC 8174: Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words.
- RFC 4033, RFC 4034 and RFC 4035: DNS Security Introduction and Requirements, Resource Records, and Protocol Modifications.
- RFC 4648: The Base16, Base32, and Base64 Data Encodings (§5, base64url).
- RFC 5011: Automated Updates of DNS Security (DNSSEC) Trust Anchors.
- RFC 5731: Extensible Provisioning Protocol (EPP) Domain Name Mapping (the
serverHoldandclientHoldstatuses). - RFC 6979: Deterministic Usage of the Digital Signature Algorithm (DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA).
- RFC 7493: The I-JSON Message Format (§2.1, surrogates and noncharacters).
- RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format.
- RFC 8785: JSON Canonicalization Scheme (JCS).
- RFC 9162: Certificate Transparency Version 2.0 (obsoletes RFC 6962).
- SEC 1: Elliptic Curve Cryptography, and SEC 2: Recommended Elliptic Curve Domain Parameters (ECDSA, secp256k1).
- D. R. L. Brown, "Generic Groups, Collision Resistance, and ECDSA", Designs, Codes and Cryptography 35, 2005.
- M. Fersch, E. Kiltz and B. Poettering, "On the Provable Security of (EC)DSA Signatures", ACM CCS 2016.
- The Update Framework (TUF) specification, version 1.0: roles, the rollback, freeze and fast-forward attacks, and root rotation.
- IPNS Record and Protocol specification: the sequence number, validity and DHT record lifetime.
- Nostr NIP-01 (replaceable events and the lowest-id rule), NIP-05 and NIP-65 (relay list metadata).
- KERI, Key Event Receipt Infrastructure: pre-rotation.
- ElectrumX protocol documentation, "Script Hashes":
SHA256(scriptPubKey), byte-reversed, in hex. - 1Sat Ordinals inscription envelope (content type
text/plain, the body in one push afterOP_0), used by the ordinal form. - Tor Rendezvous Specification, version 3: self-authenticating onion service addresses.