Blockchain Domain Service Development
Blockchain domains solve a problem that stands out especially acutely in Web3: cryptographic addresses are unreadable for humans. 0x742d35Cc6634C0532925a3b844Bc454e4438f44e — this is not an address but a source of errors. A blockchain domain service replaces the address with a human-readable name while simultaneously turning that name into a portable identity record.
DNS-like System Architecture on Blockchain
Namespace and Registry
The central component is the Registry contract. It stores the mapping from hashed names (namehash) to owner and resolver addresses. ENS uses exactly this architecture, and it is justified: separating ownership (Registry) and data storage (Resolver) allows changing the resolver without losing ownership.
contract DomainRegistry {
struct Record {
address owner;
address resolver;
uint64 ttl;
}
// namehash => Record
mapping(bytes32 => Record) private records;
// namehash => operator => approved
mapping(bytes32 => mapping(address => bool)) private operators;
event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner);
event Transfer(bytes32 indexed node, address owner);
event NewResolver(bytes32 indexed node, address resolver);
function setOwner(bytes32 node, address _owner) external authorised(node) {
records[node].owner = _owner;
emit Transfer(node, _owner);
}
function setSubnodeOwner(
bytes32 node,
bytes32 label,
address _owner
) external authorised(node) returns (bytes32) {
bytes32 subnode = keccak256(abi.encodePacked(node, label));
records[subnode].owner = _owner;
emit NewOwner(node, label, _owner);
return subnode;
}
modifier authorised(bytes32 node) {
address owner = records[node].owner;
require(
owner == msg.sender || operators[node][msg.sender],
"Not authorised"
);
_;
}
}
Namehash Algorithm
Names are transformed into bytes32 through recursive hashing. alice.myns → keccak256(keccak256('' bytes32(0)) + keccak256('myns')) → keccak256(result + keccak256('alice')). This allows computing the hash of any nesting level without knowing the full name — only its components.
import { ethers } from "ethers";
function namehash(name: string): string {
let node = "0x" + "0".repeat(64);
if (name === "") return node;
const labels = name.split(".").reverse();
for (const label of labels) {
node = ethers.utils.keccak256(
ethers.utils.concat([
node,
ethers.utils.keccak256(ethers.utils.toUtf8Bytes(label))
])
);
}
return node;
}
Resolver Contract
Resolver stores data associated with a name. One resolver can serve multiple names.
contract PublicResolver {
DomainRegistry immutable registry;
// node => coinType => address (EIP-2304: multi-chain addresses)
mapping(bytes32 => mapping(uint256 => bytes)) private _addresses;
// node => key => value (text records)
mapping(bytes32 => mapping(string => string)) private _textRecords;
// node => contenthash (IPFS/Swarm/Arweave)
mapping(bytes32 => bytes) private _contenthash;
event AddressChanged(bytes32 indexed node, uint256 coinType, bytes newAddress);
event TextChanged(bytes32 indexed node, string indexed key, string value);
event ContenthashChanged(bytes32 indexed node, bytes hash);
// Ethereum address (coinType 60 = ETH)
function setAddr(bytes32 node, address addr) external authorised(node) {
setAddr(node, 60, addressToBytes(addr));
}
// Multi-chain: BTC coinType = 0, ETH = 60, SOL = 501
function setAddr(bytes32 node, uint256 coinType, bytes calldata a)
public authorised(node)
{
_addresses[node][coinType] = a;
emit AddressChanged(node, coinType, a);
}
// Text records: "email", "url", "avatar", "description", "twitter"
function setText(bytes32 node, string calldata key, string calldata value)
external authorised(node)
{
_textRecords[node][key] = value;
emit TextChanged(node, key, value);
}
// IPFS contenthash for decentralized websites
function setContenthash(bytes32 node, bytes calldata hash) external authorised(node) {
_contenthash[node] = hash;
emit ContenthashChanged(node, hash);
}
modifier authorised(bytes32 node) {
require(registry.owner(node) == msg.sender, "Not authorised");
_;
}
}
Registrar Contract and NFT
Top-level domain names (TLD) are registered through a Registrar. Each registered name is an ERC-721 NFT, allowing trading names on OpenSea and other marketplaces.
contract BaseRegistrar is ERC721 {
DomainRegistry public registry;
bytes32 public baseNode; // namehash TLD (e.g., namehash("myns"))
mapping(uint256 => uint256) public expiries; // tokenId => expiry timestamp
uint256 public constant GRACE_PERIOD = 90 days;
function available(uint256 id) public view returns (bool) {
return expiries[id] + GRACE_PERIOD < block.timestamp;
}
function register(
uint256 id,
address owner,
uint256 duration
) external onlyController returns (uint256) {
require(available(id), "Not available");
expiries[id] = block.timestamp + duration;
if (_exists(id)) {
// If token existed before — just update expiry
_transfer(address(0), owner, id); // re-issue
} else {
_mint(owner, id);
}
registry.setSubnodeOwner(baseNode, bytes32(id), owner);
return expiries[id];
}
function renew(uint256 id, uint256 duration) external onlyController returns (uint256) {
require(expiries[id] + GRACE_PERIOD >= block.timestamp, "Expired");
expiries[id] += duration;
return expiries[id];
}
}
Price Oracle and Registration
Registration prices usually depend on name length:
contract PriceOracle {
// Price in USD/year, in wei via Chainlink ETH/USD feed
uint256[5] public rentPrices = [
160e18, // 1 character: $160/year
40e18, // 2 characters: $40/year
10e18, // 3 characters: $10/year
5e18, // 4 characters: $5/year
1e18 // 5+ characters: $1/year
];
AggregatorV3Interface public immutable usdOracle;
function price(string calldata name, uint256 duration)
external view returns (uint256 weiAmount)
{
uint256 len = strlen(name);
uint256 usdPrice = rentPrices[min(len - 1, 4)];
uint256 annualUsd = usdPrice * duration / 365 days;
(, int256 usdEthPrice,,,) = usdOracle.latestRoundData();
return annualUsd * 1e8 / uint256(usdEthPrice); // 8 decimal Chainlink feed
}
}
Reverse Resolution
Forward resolution: alice.myns → 0x742d.... Reverse resolution: 0x742d... → alice.myns. This is necessary for displaying names in interfaces.
It is implemented through a special reverse namespace: address 0x742d... is mapped to a record 742d...addr.reverse. The user himself sets the reverse record — this is his choice, which name to display.
contract ReverseRegistrar {
bytes32 constant ADDR_REVERSE_NODE =
0x91d1777781884d03a6757a803996e38de2a42967fb37eeaca72729271025a9e2;
function setName(string calldata name) external returns (bytes32) {
bytes32 node = claimWithResolver(msg.sender, address(defaultResolver));
defaultResolver.setName(node, name);
return node;
}
function node(address addr) public pure returns (bytes32) {
return keccak256(abi.encodePacked(ADDR_REVERSE_NODE, sha3HexAddress(addr)));
}
}
Subdomain Delegation
Powerful feature: domain owner can create subdomains and delegate them. team.alice.myns, dao.alice.myns — created in one transaction. Protocols use this for on-chain identity systems for participants.
NameWrapper (ENS v2 pattern) — a wrapper that turns subdomains into ERC-1155 tokens and adds a permission system: fuses. The "CANNOT_TRANSFER" fuse — name cannot be transferred (soulbound subdomain). "CANNOT_CREATE_SUBDOMAIN" fuse — cannot create second-level subdomains.
Offchain Resolver (CCIP-Read / EIP-3668)
For scalability — off-chain data storage with on-chain verification. Resolver returns an OffchainLookup error with URL and request data. Client makes a request to the off-chain gateway, gets a signed response, passes it back to the contract for signature verification.
This reduces data writing costs from on-chain gas to off-chain storage. Suitable for profile data, large amounts of text records.
Stack and Integration
| Component | Technology |
|---|---|
| Smart contracts | Solidity 0.8.x + OpenZeppelin |
| Chainlink Oracle | AggregatorV3Interface for ETH/USD |
| Frontend resolution | ethers.js provider.resolveName() |
| Indexing | The Graph subgraph |
| CCIP-Read gateway | Node.js server + ECDSA signature |
Development Timelines
Basic service (Registry + Resolver + Registrar + Price Oracle): 6-8 weeks.
Extended (NameWrapper + reverse resolution + CCIP-Read gateway + marketplace integration): 10-14 weeks.
Audit is mandatory — Registrar manages ETH payments. 2-4 weeks additional.







