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:
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" || namewhere name is the label without the parent suffix (vitalik, notvitalik.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
.solnames, parent is the PDA of the "sol" TLD record. For subdomains likebilling.acme.sol, parent is the PDA ofacme.sol.
The NameRecordHeader
Every name account starts with a fixed header:
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 dataFor 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
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
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:
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 recordEach 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:
acme.sol → parent = sol TLD PDA
billing.acme.sol → parent = acme.sol PDA
docs.acme.sol → parent = acme.sol PDAOwners 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.