How a name resolves

Getting to bytes is a ladder, and which reference you already hold decides where you step onto it. A name is the top rung. If you already have a peer identity you skip the registry entirely; if you have a content hash you skip almost everything.

What you holdWhere it entersWhat it still needs
a namethe topa registry
a peer identitythe middlea way to reach that peer
a transport addressbelow thatnothing — dial it
a content hashthe bottomany source at all

The registry's job is the top rung only. It turns a name into a peer identity, the transports to reach it at, and a trust anchor. Everything below is the network and tree layers, and none of it consults the registry again.

The rest of this page is one name resolved end to end, with the actual artifacts. It is worth reading in order — the shape of a registry is much easier to see once than to describe twice.

One thing to hold on to first, because the worked example below can easily teach the wrong lesson. A registry is a peer publishing binding entities into its own tree. That is the entire definition. Resolving a name is two tree reads and a verify:

binding_hash = tree:get(registry, "system/registry/binding/by-name/" + name)
binding      = content:get(registry, binding_hash)
verify(binding)

Those reads are transport-agnostic. This deployment answers them as HTTP GETs against a static host, because that is the cheapest shape that proves the mechanism — so that is what the URLs below show. A registry peer running live on the network answers the same reads over a session transport. A registry on your own machine answers them out of the local store with no network involved at all. The verify step is byte-for-byte identical in all three cases.

So read the URLs below as this deployment's transport, not as the registry. Where you see GET https://…, the operation is tree:get. What else a registry can be is the last section on this page, and it is the more important half.

What you start with

One string: the registry's peer identity, which your configuration holds as a pin.

2KFNrGARQBkx3d9WQtkeuT3HbQ3oXSS7PBggWt1szT9hzb

That is the whole of your trust configuration for this registry. It is not a URL and not a hostname — it is an identity, and it carries the public key that checks every signature below, so nothing has to be fetched to know who the registry is.

An origin comes along with it, but only as a hint about where to look. Nothing that follows trusts it.

Hop one — the name

The by-name pointer

Ask the registry's tree for the name. The path is the name:

GET https://entitychurchregistry.org
      /2KFNrGARQBkx3d9WQtkeuT3HbQ3oXSS7PBggWt1szT9hzb
      /system/registry/binding/by-name/billslab.com.bin

200, 58 bytes

Fifty-eight bytes, because what comes back is a pointer rather than the thing:

{
  "type": "system/hash",
  "data": h'0081473eb532483a5a92e73a585dc8838fe77add1b1dfa3b9676e8b12342376a82'
}

A tree leaf hands you the bound content hash, not the entity. You take the hash and fetch the bytes yourself. That is one indirection that looks gratuitous and is not: the same content bound at ten paths is stored once, and a static host has no way to recover that property if the tree hands out copies.

The leading 00 is the hash format, and the length follows from it — 33 bytes here, and a different width under a different format byte. It is read, never assumed.

The binding

Fetch the hash from the content store. The layout is sharded-2-4, so the first two byte pairs become directories:

GET https://entitychurchregistry.org
      /content/00/81/0081473eb532483a5a92e73a585dc8838fe77add1b1dfa3b9676e8b12342376a82

200, 209 bytes

This is a registry entry. All 209 bytes of it, decoded:

{
  "type": "system/registry/binding",
  "data": {
    "name":           "billslab.com",
    "kind":           "peer-issued",
    "target_peer_id": "2KBj64an1PvKXMafcT2rCC8eaac4Kmyht7xTeCap8USWho",
    "transports":     [ h'00ff72ef4ef8a6d61b0bff70a928e5ba79e3a29a90bd80bda68a3f6850ff87ef75' ],
    "issued_at":      <milliseconds since epoch>,
    "ttl":            2592000000
  }
}

Six fields. A name, the identity it means, a pointer to how you reach that identity, when it was issued and how long it is good for.

Nothing in the body is a URL, and that is deliberate — the entry says who, and the thing it points at says where.

The signature

The commitment lives at a path derived from the binding's own hash:

GET https://entitychurchregistry.org
      /2KFNrGARQBkx3d9WQtkeuT3HbQ3oXSS7PBggWt1szT9hzb
      /system/signature/0081473eb532483a5a92e73a585dc8838fe77add1b1dfa3b9676e8b12342376a82.bin

200, 58 bytes

Fifty-eight bytes again — another pointer, dereferenced the same way. The signature is over the binding body, and the body contains the name, so the signature is the assertion that this name means this peer.

What your client checks before believing any of it

Every check below is against a field you have already seen:

  1. The bytes hash to the hash you asked for. Otherwise you fetched something else.
  2. The signature targets this binding and was made by the registry you pinned.
  3. binding.name is the name you asked for. This is the one that looks redundant and is not, so it gets its own section below.
  4. ttl is present and finite.
  5. issued_at + ttl is still in the future.
  6. No revocation is published against this binding hash.

Any failure is a dead end, not a downgrade. Your resolver moves to the next configured registry; it does not fall back to trusting the origin.

Why check the name when the signature already verified

Because the signature proves who issued a binding, never what it was issued for.

The pointer file at by-name/billslab.com.bin is served by a host. A host chooses which bytes answer which URL. So a hostile origin repoints that one file at a different binding — one the registry genuinely signed, for a different name — and every other check passes: valid signature, correct signer, unexpired, unrevoked, wrong name.

The fix costs nothing, because the registry already committed to the pairing when it signed a body containing name. The defect was never a missing commitment; it was a resolver decoding the body, reading ttl and target_peer_id out of it, and throwing away the one field that ties the answer to the question.

Hop two — the content

You now have a peer identity and a hash pointing at how to reach it. Fetch the profile the same way you fetched the binding:

GET https://entitychurchregistry.org
      /content/00/ff/00ff72ef4ef8a6d61b0bff70a928e5ba79e3a29a90bd80bda68a3f6850ff87ef75

200, 628 bytes
{
  "type": "system/peer/transport/http-poll",
  "data": {
    "peer_id":        "2KBj64an1PvKXMafcT2rCC8eaac4Kmyht7xTeCap8USWho",
    "transport_type": "http-poll",
    "endpoint": {
      "tree_url_prefix":     "https://billslab.com/2KBj64an1PvKXMafcT2rCC8eaac4Kmyht7xTeCap8USWho",
      "content_url_prefix":  "https://billslab.com/content",
      "manifest_url_prefix": "https://billslab.com/2KBj64an1PvKXMafcT2rCC8eaac4Kmyht7xTeCap8USWho/system/peer/published-root",
      "content_layout":      "sharded-2-4",
      "tree_leaf_suffix":    ".bin",
      "tree_listing_suffix": ".list"
    },
    "freshness":      "static-immutable+signed-pointer",
    "supported_ops":  ["TREE_GET", "CONTENT_GET", "MANIFEST_GET"],
    "nonce_required": false,
    "signed_pointer": "system/peer/published-root",
    "cap_flow":       "egress"
  }
}

This is the hop that makes a name mean something operationally, and it is where the whole mechanism usually stops being abstract. You asked about a name on one domain and you are now holding fetchable URLs on a different one. The registry never proxied anything; it handed you an address and got out of the way.

Read it plainly: supported_ops says this publisher answers reads and nothing else — no writes, no subscriptions, no session. freshness says the bytes never change and a signed pointer tells you which set is current. nonce_required: false follows from there being no session to have a nonce in. None of that needed a running peer to negotiate; it is a file.

Two of the fields are worth not confusing. signed_pointer is a tree path naming what the origin is asserting. manifest_url_prefix is where you GET it. They are not joinable, and a consumer that appends the first to an origin fails at hop zero against every publisher that does not share its convention.

So the last hop is the signed root:

GET https://billslab.com
      /2KBj64an1PvKXMafcT2rCC8eaac4Kmyht7xTeCap8USWho/system/peer/published-root

200, 273 bytes

From there it is an ordinary content walk down that peer's tree, recomputing every hash, with the registry no longer involved in any of it.

Prefixes, not a hostname

The three prefixes are the reason one publish works unmodified in three places. In a local build they are relative:

"tree_url_prefix":    "/2KBj64an1PvKXMafcT2rCC8eaac4Kmyht7xTeCap8USWho"
"content_url_prefix": "/content"

Same tree, same bytes, same walk — served from the same origin as the page doing the reading. In production they carry a real hostname, and the fetch above becomes cross-origin. That is the entire difference between a registry that works on a laptop and one that has to survive a browser, and it is where the interesting failures live.

They also need not agree with each other. Tree on one host, content on a shared dedup bucket, manifest somewhere else again — the profile says where each one is, so nothing downstream has to guess.

The origin is trusted for nothing

Both hops walk from a signed root, and the hashes decide. A host that repoints a file produces a verification failure, not a different page. This is why the registry can be served from ordinary static hosting with no live process behind it: what you just walked was a directory of files on a CDN, and its authority is in the signatures, not in the server.

One consequence is easy to miss and worth stating: a resolver has to be able to read a 404. Absence is an answer — it is how "this registry does not carry that name" is distinguished from "something went wrong" — so the not-found response has to be as readable, cross-origin, as the successful one. A 404 the browser refuses to show you is indistinguishable from a network failure, and a resolver that cannot tell those apart cannot fail closed honestly.

What it does not buy

A registry does not guarantee freshness. A host that serves an older signed root is serving something that verifies correctly and is out of date. Within one session a client can hold a floor and refuse to go backwards; across a cold start, the bound is the expiry on the binding.

That is what ttl is for, and why it may not be null. A hostile host cannot forge a signature, alter a body or move your clock — but it can withhold a revocation indefinitely, and a withheld revocation is byte-identical to one that was never issued. The TTL is the only bound on that. Set it to null and the bound is not weak, it is absent, and the binding is permanently unrevokable.

Nor does resolution confer trust. It tells you that a registry you chose signed a statement associating this name with this peer. Whether that registry should have, and whether the peer is what you wanted, are questions the chain does not answer.

None of this requires HTTP, a static host, or anyone else being able to look

Everything above was one deployment's transport. Strip that away and the registry is unchanged: a peer, publishing signed bindings into its tree, answering two reads.

A registry can be a live peer. Nothing about the design prefers static files. A registry running as an ordinary peer on the network answers tree:get and content:get over a session transport, and can do things a directory of files cannot — issue a binding on request, revoke one the moment it decides to, push an invalidation to subscribers instead of waiting out a TTL. The static shape trades those away for costing nothing to run. It is a deployment choice, not the mechanism.

A registry can be private. There is no obligation to let anyone read yours, and no step above requires it. A registry serving one company's internal names, reachable only inside its network, is a registry in exactly the same sense as this one — same binding entities, same signatures, same verification. Publishing is what this deployment does because a demonstration nobody can inspect demonstrates nothing.

A registry can be entirely local. Bindings pre-cached on your own machine resolve with no network at any point: the two reads hit the local store, and the verify is the same code doing the same checks. This is how a client resolves before it has ever spoken to anything, and it is why a name can resolve offline.

And a name need not come from a registry at all. You can bind one yourself, as an assertion to yourself — no signature, no issuer, no meaning off your machine, and it resolves fine. Or pin a name directly in your configuration and skip resolution entirely.

The through-line: the binding is the substance, and everything else is delivery. A signed body saying this name means this peer is worth the same whether it arrived over HTTPS from a CDN, over a socket from a peer that minted it seconds ago, or off your own disk having never touched a network. That is the property worth taking away from this page — the URLs were only the version of it we could show you.

For the normative treatment — the resolver contract, the binding shape, the trust evidence model and the backends that compose on them — see the registry extension and the resolution guide in the architecture corpus at entitychurchfoundation.org.

None of the above is specific to this registry. It is the mechanism any registry implements, which is why running your own changes nothing about how a client resolves against it.