All articles
solanasnsbonfidanaming

SNS: Solana Name Service, the registry program, and how .sol resolves

SNS maps .sol names to Solana addresses via Bonfida's Name Service program. Here's how a .sol name is derived from a hash, and how to resolve one in code.

SNS (Solana Name Service) is the .sol naming system every Solana wallet implements. Type vitalik.sol into Phantom's send field and you get a Solana address back — that resolution chain runs through a single program, the SPL Name Service.

Operationally it's maintained by Bonfida's SNS, but the underlying program is permissionless and exposes a general hierarchical naming primitive (anyone could build a .eth or .app hierarchy on top of it).

The program

The SPL Name Service program is at namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX. It owns every name record on Solana, in any namespace. Two related Bonfida programs handle the .sol-specific business logic (registration, auctions, subdomains).

How a name maps to a PDA

Every name record is a PDA, derived from:

text
hash    = SHA-256("SPL Name Service" || name)
PDA seeds = [hash, class_pubkey, parent_pubkey]
PDA       = find_program_address(seeds, NameServiceProgram)

Three things go into the derivation:

  • hash — SHA-256 of "SPL Name Service" || name where name is the label without the parent suffix (vitalik, not vitalik.sol)
  • class_pubkey — optional "name class" for distinguishing record types (zero pubkey for plain ownership records, non-zero for record types like Twitter handles or IPFS pointers)
  • parent_pubkey — the PDA of the parent name in the hierarchy. For .sol names, parent is the PDA of the "sol" TLD record. For subdomains like billing.acme.sol, parent is the PDA of acme.sol.

The NameRecordHeader

Every name account starts with a fixed header:

rust
pub struct NameRecordHeader {
    pub parent_name: Pubkey,    // 32 — the parent name PDA, or zero
    pub owner:       Pubkey,    // 32 — current owner; this is the resolution result
    pub class:       Pubkey,    // 32 — the name class
}
// Header = 96 bytes; the rest of the account is application-defined data

For a plain .sol name, the owner field is the Solana address the name resolves to. Everything past the 96-byte header is optional metadata (Bonfida uses it for additional records like Twitter handles, IPFS hashes, etc).

Resolving a .sol address — full flow

typescript
import { Connection, PublicKey } from "@solana/web3.js"
import { createHash } from "crypto"

const NAME_PROGRAM_ID = new PublicKey("namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX")
const ZERO = new PublicKey(new Uint8Array(32))

// 1. Derive the .sol TLD PDA (the parent of all .sol names)
function hashName(name: string): Buffer {
  return createHash("sha256")
    .update("SPL Name Service" + name)
    .digest()
}

const solTldHash = hashName("sol")
const [solTld] = PublicKey.findProgramAddressSync(
  [solTldHash, ZERO.toBuffer(), ZERO.toBuffer()],
  NAME_PROGRAM_ID,
)

// 2. Derive the name PDA for "vitalik" under .sol
const labelHash = hashName("vitalik")
const [namePda] = PublicKey.findProgramAddressSync(
  [labelHash, ZERO.toBuffer(), solTld.toBuffer()],
  NAME_PROGRAM_ID,
)

// 3. Fetch the account and read the owner from offset 32
const conn = new Connection("https://api.mainnet-beta.solana.com")
const info = await conn.getAccountInfo(namePda)
if (!info) throw new Error("name not registered")

const ownerBytes = info.data.subarray(32, 64)
const owner = new PublicKey(ownerBytes)
console.log("vitalik.sol resolves to:", owner.toBase58())

The Bonfida JS shortcut

typescript
import { resolve } from "@bonfida/spl-name-service"
import { Connection } from "@solana/web3.js"

const conn = new Connection("https://api.mainnet-beta.solana.com")
const owner = await resolve(conn, "vitalik.sol")
console.log(owner.toBase58())

The Bonfida SDK handles the derivation pipeline plus modern features like favourite names, record types (SOL address vs IPV4 vs Twitter handle vs etc), and subdomain resolution.

Records — typed data on a name

Beyond the basic owner address, SNS supports records — typed data attached to a name, each stored as its own sub-account under the name with a specific class:

text
Common Record types:
  SOL          — secondary Solana address (vs owner)
  ETH          — Ethereum address
  BTC          — Bitcoin address
  email        — email
  url          — website URL
  IPFS         — IPFS content hash
  ARWV         — Arweave hash
  CNAME        — alias to another .sol name
  TWITTER      — Twitter handle
  GITHUB       — GitHub username
  TXT          — generic text record

Each record is its own PDA, derived with the record type as the name and the parent .sol name as the parent. Wallets resolve records lazily — only fetch the ones you care about.

Subdomain hierarchy

Subdomains chain through the parent_name field:

text
acme.sol         →  parent = sol TLD PDA
billing.acme.sol →  parent = acme.sol PDA
docs.acme.sol    →  parent = acme.sol PDA

Owners of acme.sol can create subdomains under it permissionlessly (call create_name with the parent set to acme.sol's PDA). Useful for organisational namespaces — every employee gets a name.company.sol without occupying the global .sol registry.

Why this design choice

Three structural advantages:

  • O(1) resolution. Given a name, you compute the PDA deterministically and fetch one account. No on-chain loop, no recursive lookup (until you traverse subdomains explicitly).
  • Hierarchical by construction. The parent pubkey is part of the PDA derivation, so subdomain namespaces don't collide with the parent.
  • General-purpose. Nothing about the program is .sol-specific. The TLD is just "the name 'sol' under zero parent" — and anyone could deploy a parallel TLD by registering a name with a different label under the zero parent. Bonfida runs the .sol social layer (auctions, registrations) on top of the neutral program.

References

SNS is a tiny, well-designed primitive that the entire Solana wallet ecosystem builds on. Once you see theSHA-256(hash) + class + parent → PDA derivation, everything else falls into place.