Розробка сервісу блокчейн-доменів
Сервіс блокчейн-доменів розв'язує проблему, яка особливо гостро стоїть у Web3: криптографічні адреси нечитаємі для людини. 0x742d35Cc6634C0532925a3b844Bc454e4438f44e — це не адреса, а джерело помилок. Сервіс блокчейн-домену замінює адресу на людиночитаному імені, водночас перетворюючи це ім'я на портативний запис ідентичності.
Архітектура DNS-подібної системи на блокчейні
Простір імен та реєстр
Центральний компонент — контракт Registry. Він зберігає маппінг від хешованого імені (namehash) до адреси власника та resolver. ENS використовує саме цю архітектуру, і вона виправдана: розділення власництва (Registry) та зберігання даних (Resolver) дозволяє змінювати resolver без втрати власництва.
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
Імена перетворюються на bytes32 через рекурсивний хеш. alice.myns → keccak256(keccak256('' bytes32(0)) + keccak256('myns')) → keccak256(result + keccak256('alice')). Це дозволяє обчислювати хеш будь-якого рівня вкладення без знання повного імені — лише його компонентів.
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
Resolver зберігає дані, пов'язані з іменем. Один resolver може обслуговувати багато імен.
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);
}
// Текстові записи: "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 для децентралізованих сайтів
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 та NFT
Домени верхнього рівня (TLD) реєструються через Registrar. Кожне зареєстроване ім'я — це ERC-721 NFT, що дозволяє торгувати іменами на OpenSea та інших маркетплейсах.
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)) {
// Якщо токен існував раніше — просто оновлюємо 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 та реєстрація
Ціни реєстрації зазвичай залежать від довжини імені:
contract PriceOracle {
// Ціна в USD/рік, у wei через Chainlink ETH/USD feed
uint256[5] public rentPrices = [
160e18, // 1 символ: $160/рік
40e18, // 2 символи: $40/рік
10e18, // 3 символи: $10/рік
5e18, // 4 символи: $5/рік
1e18 // 5+ символів: $1/рік
];
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
}
}
Зворотне розв'язування
Пряме розв'язування: alice.myns → 0x742d.... Зворотне розв'язування: 0x742d... → alice.myns. Це необхідно для відображення імен в інтерфейсах.
Реалізується через спеціальний зворотний простір імен: адреса 0x742d... маппується на запис 742d...addr.reverse. Користувач сам встановлює зворотний запис — це його вибір, яке ім'я показувати.
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)));
}
}
Делегування поддоменів
Потужна функція: власник домену може створювати поддомени та делегувати їх. team.alice.myns, dao.alice.myns — створюються за одну транзакцію. Протоколи використовують це для on-chain систем ідентичності учасників.
NameWrapper (паттерн ENS v2) — обгортка, яка перетворює поддомени на ERC-1155 токени та додає систему дозволів: fuses. Fuse "CANNOT_TRANSFER" — ім'я неможливо передати (soulbound поддомен). Fuse "CANNOT_CREATE_SUBDOMAIN" — неможливо створити поддомени другого рівня.
Offchain resolver (CCIP-Read / EIP-3668)
Для масштабованості — off-chain зберігання даних з on-chain верифікацією. Resolver повертає помилку OffchainLookup з URL та даними запиту. Клієнт робить запит до off-chain gateway, отримує підписану відповідь, передає її назад у контракт для верифікації підпису.
Це зменшує вартість запису даних з on-chain газу до off-chain зберігання. Підходить для даних профілю, великої кількості текстових записів.
Стек та інтеграція
| Компонент | Технологія |
|---|---|
| Smart contracts | Solidity 0.8.x + OpenZeppelin |
| Chainlink Oracle | AggregatorV3Interface для ETH/USD |
| Frontend resolution | ethers.js provider.resolveName() |
| Indexing | The Graph subgraph |
| CCIP-Read gateway | Node.js сервер + ECDSA підпис |
Графіки розробки
Базовий сервіс (Registry + Resolver + Registrar + Price Oracle): 6-8 тижнів.
Розширений (NameWrapper + зворотне розв'язування + CCIP-Read gateway + інтеграція маркетплейсу): 10-14 тижнів.
Аудит є обов'язковим — Registrar управляє платежами у ETH. Додаткові 2-4 тижні.







