Разработка сервиса блокчейн-доменов
Блокчейн-домены решают проблему, которая в Web3 стоит особенно остро: криптографические адреса нечитаемы для человека. 0x742d35Cc6634C0532925a3b844Bc454e4438f44e — это не адрес, а источник ошибок. Blockchain domain service заменяет адрес на человекочитаемое имя, одновременно превращая это имя в portable identity record.
Архитектура DNS-like системы на блокчейне
Namespace и registry
Центральный компонент — Registry контракт. Хранит маппинг от хешированного имени (namehash) к owner и resolver адресу. ENS использует именно эту архитектуру, и она оправдана: разделение ownership (Registry) и data storage (Resolver) позволяет менять resolver без потери 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 алгоритм
Имена преобразуются в 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 (текстовые записи)
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
}
}
Reverse Resolution
Forward resolution: alice.myns → 0x742d.... Reverse resolution: 0x742d... → alice.myns. Это необходимо для отображения имён в интерфейсах.
Реализуется через специальный reverse namespace: адрес 0x742d... маппируется на запись 742d...addr.reverse. Пользователь сам устанавливает reverse record — это его выбор, какое имя показывать.
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 делегирование
Мощная фича: владелец домена может создавать субдомены и делегировать их. team.alice.myns, dao.alice.myns — создаются одной транзакцией. Протоколы используют это для on-chain identity системы участников.
NameWrapper (паттерн ENS v2) — обёртка, которая превращает субдомены в ERC-1155 токены и добавляет permission system: fuses. Fuse "CANNOT_TRANSFER" — имя нельзя передать (soulbound subdomain). Fuse "CANNOT_CREATE_SUBDOMAIN" — нельзя создать субдомены второго уровня.
Offchain resolver (CCIP-Read / EIP-3668)
Для масштабируемости — off-chain хранение данных с on-chain верификацией. Resolver возвращает ошибку OffchainLookup с URL и данными запроса. Клиент делает запрос к off-chain gateway, получает подписанный ответ, передаёт его обратно в контракт для верификации подписи.
Это снижает стоимость записи данных с on-chain gas до off-chain storage. Подходит для profilge данных, большого количества текстовых записей.
Стек и интеграция
| Компонент | Технология |
|---|---|
| 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 + reverse resolution + CCIP-Read gateway + marketplace интеграция): 10-14 недель.
Аудит обязателен — Registrar управляет ETH payments. 2-4 недели дополнительно.







