跳到主要内容

TON DNS 解析器

介绍

TON DNS 是一个强大的工具。它不仅允许将 TON 网站/存储包分配给域名,还可以设置子域名解析。

相关链接

  1. TON 智能合约地址系统
  2. TEP-0081 - TON DNS 标准
  3. .ton DNS 集合的源代码
  4. .t.me DNS 集合的源代码
  5. 域名合约搜索器
  6. 简单子域名管理器代码

域名合约搜索器

子域名具有实际用途。例如,区块链浏览器目前没有提供通过名称查找域名合约的方法。让我们探索如何创建一个合约,提供查找这类域名的机会。

信息

This contract is deployed at EQDkAbAZNb4uk-6pzTPDO2s0tXZweN-2R08T2Wy6Z3qzH_Zp and linked to resolve-contract.ton. To test it, you may write <your-domain.ton>.resolve-contract.ton in the address bar of your favourite TON explorer and get to the page of TON DNS domain contract. Subdomains and .t.me domains are supported as well.

您可以尝试通过访问 resolve-contract.ton.resolve-contract.ton 来查看解析器代码。不幸的是,这将不会显示子解析器(那是不同的智能合约),您将看到域名合约本身的页面。

dnsresolve() 代码

部分重复部分已省略。

(int, cell) dnsresolve(slice subdomain, int category) method_id {
int subdomain_bits = slice_bits(subdomain);
throw_unless(70, (subdomain_bits % 8) == 0);

int starts_with_zero_byte = subdomain.preload_int(8) == 0; ;; assuming that 'subdomain' is not empty
if (starts_with_zero_byte) {
subdomain~load_uint(8);
if (subdomain.slice_bits() == 0) { ;; current contract has no DNS records by itself
return (8, null());
}
}

;; we are loading some subdomain
;; supported subdomains are "ton\0", "me\0t\0" and "address\0"

slice subdomain_sfx = null();
builder domain_nft_address = null();

if (subdomain.starts_with("746F6E00"s)) {
;; we're resolving
;; "ton" \0 <subdomain> \0 [subdomain_sfx]
subdomain~skip_bits(32);

;; reading domain name
subdomain_sfx = subdomain;
while (subdomain_sfx~load_uint(8)) { }

subdomain~skip_last_bits(8 + slice_bits(subdomain_sfx));

domain_nft_address = get_ton_dns_nft_address_by_index(slice_hash(subdomain));
} elseif (subdomain.starts_with("6164647265737300"s)) {
subdomain~skip_bits(64);

domain_nft_address = subdomain~decode_base64_address_to(begin_cell());

subdomain_sfx = subdomain;
if (~ subdomain_sfx.slice_empty?()) {
throw_unless(71, subdomain_sfx~load_uint(8) == 0);
}
} else {
return (0, null());
}

if (slice_empty?(subdomain_sfx)) {
;; example of domain being resolved:
;; [initial, not accessible in this contract] "ton\0resolve-contract\0ton\0ratelance\0"
;; [what is accessible by this contract] "ton\0ratelance\0"
;; subdomain "ratelance"
;; subdomain_sfx ""

;; we want the resolve result to point at contract of 'ratelance.ton', not its owner
;; so we must answer that resolution is complete + "wallet"H is address of 'ratelance.ton' contract

;; dns_smc_address#9fd3 smc_addr:MsgAddressInt flags:(## 8) { flags <= 1 } cap_list:flags . 0?SmcCapList = DNSRecord;
;; _ (HashmapE 256 ^DNSRecord) = DNS_RecordSet;

cell wallet_record = begin_cell().store_uint(0x9fd3, 16).store_builder(domain_nft_address).store_uint(0, 8).end_cell();

if (category == 0) {
cell dns_dict = new_dict();
dns_dict~udict_set_ref(256, "wallet"H, wallet_record);
return (subdomain_bits, dns_dict);
} elseif (category == "wallet"H) {
return (subdomain_bits, wallet_record);
} else {
return (subdomain_bits, null());
}
} else {
;; subdomain "resolve-contract"
;; subdomain_sfx "ton\0ratelance\0"
;; we want to pass \0 further, so that next resolver has opportunity to process only one byte

;; next resolver is contract of 'resolve-contract<.ton>'
;; dns_next_resolver#ba93 resolver:MsgAddressInt = DNSRecord;
cell resolver_record = begin_cell().store_uint(0xba93, 16).store_builder(domain_nft_address).end_cell();
return (subdomain_bits - slice_bits(subdomain_sfx) - 8, resolver_record);
}
}

dnsresolve() 解释

  • 用户请求 "stabletimer.ton.resolve-contract.ton"
  • 应用程序将其转换为 "\0ton\0resolve-contract\0ton\0stabletimer\0"(第一个零字节是可选的)。
  • 根 DNS 解析器将请求定向到 TON DNS 集合,剩余部分为 "\0resolve-contract\0ton\0stabletimer\0"
  • TON DNS 集合将请求委托给特定域名,留下 "\0ton\0stabletimer\0"
  • .TON DNS 域名合约将解析传递给编辑器指定的子解析器,子域名为 "ton\0stabletimer\0"

这是 dnsresolve() 被调用的点。 分步解释其工作方式:

  1. 它将子域名和类别作为输入。
  2. 如果开头有零字节,则跳过。
  3. 检查子域名是否以 "ton\0" 开头。如果是,
    1. 跳过前32位(子域名 = "resolve-contract\0"
    2. 设置 subdomain_sfx 的值为 subdomain,并读取直到零字节的字节
    3. (子域名 = "resolve-contract\0",subdomain_sfx = ""
    4. 从子域名切片的末尾裁剪零字节和 subdomain_sfx(子域名 = "resolve-contract"
    5. 使用 slice_hash 和 get_ton_dns_nft_address_by_index 函数将域名转换为合约地址。您可以在 [[Subresolvers#Appendix 1. resolve-contract.ton 的代码|附录 1]] 中看到它们。
  4. 否则,dnsresolve() 检查子域名是否以 "address\0" 开头。如果是,它跳过该前缀并读取 base64 地址。
  5. 如果提供的用于解析的子域名与这些前缀都不匹配,函数通过返回 (0, null())(零字节前缀解析无 DNS 条目)表示失败。
  6. 然后检查子域名后缀是否为空。空后缀表示请求已完全满足。如果后缀为空:
    1. dnsresolve() 为域名的 "wallet" 子部分创建一个 DNS 记录,使用它检索到的 TON 域名合约地址。
    2. 如果请求类别 0(所有 DNS 条目),则将记录包装在字典中并返回。
    3. 如果请求类别为 "wallet"H,则按原样返回记录。
    4. 否则,指定类别没有 DNS 条目,因此函数表示解析成功但未找到任何结果。
  7. 如果后缀不为空:
    1. 之前获得的合约地址用作下一个解析器。函数构建指向它的下一个解析器记录。
    2. "\0ton\0stabletimer\0" 被传递给该合约:处理的位是子域名的位。

总结来说,dnsresolve() 要么:

  • 将子域名完全解析为 DNS 记录
  • 部分解析为解析器记录,以将解析传递给另一个合约
  • 为未知子域名返回“未找到域名”的结果
危险

实际上,base64 地址解析不起作用:如果您尝试输入 <some-address>.address.resolve-contract.ton,您将收到一个错误,表明域名配置错误或不存在。原因是域名不区分大小写(从真实 DNS 继承的功能),因此会转换为小写,将您带到不存在的工作链的某个地址。

绑定解析器

现在子解析器合约已部署,我们需要将域名指向它,即更改域名的 dns_next_resolver 记录。我们可以通过将以下 TL-B 结构的消息发送到域名合约来实现。

  1. change_dns_record#4eb1f0f9 query_id:uint64 record_key#19f02441ee588fdb26ee24b2568dd035c3c9206e11ab979be62e55558a1d17ff record:^[dns_next_resolver#ba93 resolver:MsgAddressInt]

创建自己的子域名管理器

子域名对普通用户来说可能有用 - 例如,将几个项目链接到单个域名,或链接到朋友的钱包。

合约数据

我们需要在合约数据中存储所有者的地址和 域名->记录哈希->记录值 字典。

global slice owner;
global cell domains;

() load_data() impure {
slice ds = get_data().begin_parse();
owner = ds~load_msg_addr();
domains = ds~load_dict();
}
() save_data() impure {
set_data(begin_cell().store_slice(owner).store_dict(domains).end_cell());
}

处理记录更新

const int op::update_record = 0x537a3491;
;; op::update_record#537a3491 domain_name:^Cell record_key:uint256
;; value:(Maybe ^Cell) = InMsgBody;

() recv_internal(cell in_msg, slice in_msg_body) {
if (in_msg_body.slice_empty?()) { return (); } ;; simple money transfer

slice in_msg_full = in_msg.begin_parse();
if (in_msg_full~load_uint(4) & 1) { return (); } ;; bounced message

slice sender = in_msg_full~load_msg_addr();
load_data();
throw_unless(501, equal_slices(sender, owner));

int op = in_msg_body~load_uint(32);
if (op == op::update_record) {
slice domain = in_msg_body~load_ref().begin_parse();
(cell records, _) = domains.udict_get_ref?(256, string_hash(domain));

int key = in_msg_body~load_uint(256);
throw_if(502, key == 0); ;; cannot update "all records" record

if (in_msg_body~load_uint(1) == 1) {
cell value = in_msg_body~load_ref();
records~udict_set_ref(256, key, value);
} else {
records~udict_delete?(256, key);
}

domains~udict_set_ref(256, string_hash(domain), records);
save_data();
}
}

我们检查传入消息是否包含某些请求,不是弹回的,来自所有者,且请求为 op::update_record

然后,我们从消息中加载域名。我们不能将域名按原样存储在字典中:它们可能有不同的长度,但 TVM 非前缀字典只能包含等长的键。因此,我们计算 string_hash(domain) - 域名的 SHA-256;域名保证有整数个八位字节,因此这是有效的。

之后,我们为指定域名更新记录,并将新数据保存到合约存储中。

解析域名

(slice, slice) ~parse_sd(slice subdomain) {
;; "test\0qwerty\0" -> "test" "qwerty\0"
slice subdomain_sfx = subdomain;
while (subdomain_sfx~load_uint(8)) { } ;; searching zero byte
subdomain~skip_last_bits(slice_bits(subdomain_sfx));
return (subdomain, subdomain_sfx);
}

(int, cell) dnsresolve(slice subdomain, int category) method_id {
int subdomain_bits = slice_bits(subdomain);
throw_unless(70, subdomain_bits % 8 == 0);
if (subdomain.preload_uint(8) == 0) { subdomain~skip_bits(8); }

slice subdomain_suffix = subdomain~parse_sd(); ;; "test\0" -> "test" ""
int subdomain_suffix_bits = slice_bits(subdomain_suffix);

load_data();
(cell records, _) = domains.udict_get_ref?(256, string_hash(subdomain));

if (subdomain_suffix_bits > 0) { ;; more than "<SUBDOMAIN>\0" requested
category = "dns_next_resolver"H;
}

int resolved = subdomain_bits - subdomain_suffix_bits;

if (category == 0) { ;; all categories are requested
return (resolved, records);
}

(cell value, int found) = records.udict_get_ref?(256, category);
return (resolved, value);
}

dnsresolve 函数检查请求的子域名是否包含整数个八位字节,跳过子域名切片开头的可选零字节,然后将其分割为最高级别的域和其他部分(test\0qwerty\0 被分割为 testqwerty\0)。加载与请求的域名对应的记录字典。

如果存在非空子域名后缀,函数返回已解析的字节数和在 "dns_next_resolver"H 键下找到的下一个解析器记录。否则,函数返回已解析的字节数(即整个切片长度)和请求的记录。

可以通过更优雅地处理错误来改进此函数,但这不是绝对必需的。

附录 1. resolve-contract.ton 的代码

Details
(builder, ()) ~store_slice(builder to, slice s) asm "STSLICER";
int starts_with(slice a, slice b) asm "SDPFXREV";

const slice ton_dns_minter = "EQC3dNlesgVD8YbAazcauIrXBPfiVhMMr5YYk2in0Mtsz0Bz"a;
cell ton_dns_domain_code() asm """
B{<TON DNS NFT code in HEX format>}
B>boc
PUSHREF
""";

const slice tme_minter = "EQCA14o1-VWhS2efqoh_9M1b_A9DtKTuoqfmkn83AbJzwnPi"a;
cell tme_domain_code() asm """
B{<T.ME NFT code in HEX format>}
B>boc
PUSHREF
""";

cell calculate_ton_dns_nft_item_state_init(int item_index) inline {
cell data = begin_cell().store_uint(item_index, 256).store_slice(ton_dns_minter).end_cell();
return begin_cell().store_uint(0, 2).store_dict(ton_dns_domain_code()).store_dict(data).store_uint(0, 1).end_cell();
}

cell calculate_tme_nft_item_state_init(int item_index) inline {
cell config = begin_cell().store_uint(item_index, 256).store_slice(tme_minter).end_cell();
cell data = begin_cell().store_ref(config).store_maybe_ref(null()).end_cell();
return begin_cell().store_uint(0, 2).store_dict(tme_domain_code()).store_dict(data).store_uint(0, 1).end_cell();
}

builder calculate_nft_item_address(int wc, cell state_init) inline {
return begin_cell()
.store_uint(4, 3)
.store_int(wc, 8)
.store_uint(cell_hash(state_init), 256);
}

builder get_ton_dns_nft_address_by_index(int index) inline {
cell state_init = calculate_ton_dns_nft_item_state_init(index);
return calculate_nft_item_address(0, state_init);
}

builder get_tme_nft_address_by_index(int index) inline {
cell state_init = calculate_tme_nft_item_state_init(index);
return calculate_nft_item_address(0, state_init);
}

(slice, builder) decode_base64_address_to(slice readable, builder target) inline {
builder addr_with_flags = begin_cell();
repeat(48) {
int char = readable~load_uint(8);
if (char >= "a"u) {
addr_with_flags~store_uint(char - "a"u + 26, 6);
} elseif ((char == "_"u) | (char == "/"u)) {
addr_with_flags~store_uint(63, 6);
} elseif (char >= "A"u) {
addr_with_flags~store_uint(char - "A"u, 6);
} elseif (char >= "0"u) {
addr_with_flags~store_uint(char - "0"u + 52, 6);
} else {
addr_with_flags~store_uint(62, 6);
}
}

slice addr_with_flags = addr_with_flags.end_cell().begin_parse();
addr_with_flags~skip_bits(8);
addr_with_flags~skip_last_bits(16);

target~store_uint(4, 3);
target~store_slice(addr_with_flags);
return (readable, target);
}

slice decode_base64_address(slice readable) method_id {
(slice _remaining, builder addr) = decode_base64_address_to(readable, begin_cell());
return addr.end_cell().begin_parse();
}

(int, cell) dnsresolve(slice subdomain, int category) method_id {
int subdomain_bits = slice_bits(subdomain);

throw_unless(70, (subdomain_bits % 8) == 0);

int starts_with_zero_byte = subdomain.preload_int(8) == 0; ;; assuming that 'subdomain' is not empty
if (starts_with_zero_byte) {
subdomain~load_uint(8);
if (subdomain.slice_bits() == 0) { ;; current contract has no DNS records by itself
return (8, null());
}
}

;; we are loading some subdomain
;; supported subdomains are "ton\0", "me\0t\0" and "address\0"

slice subdomain_sfx = null();
builder domain_nft_address = null();

if (subdomain.starts_with("746F6E00"s)) {
;; we're resolving
;; "ton" \0 <subdomain> \0 [subdomain_sfx]
subdomain~skip_bits(32);

;; reading domain name
subdomain_sfx = subdomain;
while (subdomain_sfx~load_uint(8)) { }

subdomain~skip_last_bits(8 + slice_bits(subdomain_sfx));

domain_nft_address = get_ton_dns_nft_address_by_index(slice_hash(subdomain));
} elseif (subdomain.starts_with("6D65007400"s)) {
;; "t" \0 "me" \0 <subdomain> \0 [subdomain_sfx]
subdomain~skip_bits(40);

;; reading domain name
subdomain_sfx = subdomain;
while (subdomain_sfx~load_uint(8)) { }

subdomain~skip_last_bits(8 + slice_bits(subdomain_sfx));

domain_nft_address = get_tme_nft_address_by_index(string_hash(subdomain));
} elseif (subdomain.starts_with("6164647265737300"s)) {
subdomain~skip_bits(64);

domain_nft_address = subdomain~decode_base64_address_to(begin_cell());

subdomain_sfx = subdomain;
if (~ subdomain_sfx.slice_empty?()) {
throw_unless(71, subdomain_sfx~load_uint(8) == 0);
}
} else {
return (0, null());
}

if (slice_empty?(subdomain_sfx)) {
;; example of domain being resolved:
;; [initial, not accessible in this contract] "ton\0resolve-contract\0ton\0ratelance\0"
;; [what is accessible by this contract] "ton\0ratelance\0"
;; subdomain "ratelance"
;; subdomain_sfx ""

;; we want the resolve result to point at contract of 'ratelance.ton', not its owner
;; so we must answer that resolution is complete + "wallet"H is address of 'ratelance.ton' contract

;; dns_smc_address#9fd3 smc_addr:MsgAddressInt flags:(## 8) { flags <= 1 } cap_list:flags . 0?SmcCapList = DNSRecord;
;; _ (HashmapE 256 ^DNSRecord) = DNS_RecordSet;

cell wallet_record = begin_cell().store_uint(0x9fd3, 16).store_builder(domain_nft_address).store_uint(0, 8).end_cell();

if (category == 0) {
cell dns_dict = new_dict();
dns_dict~udict_set_ref(256, "wallet"H, wallet_record);
return (subdomain_bits, dns_dict);
} elseif (category == "wallet"H) {
return (subdomain_bits, wallet_record);
} else {
return (subdomain_bits, null());
}
} else {
;; example of domain being resolved:
;; [initial, not accessible in this contract] "ton\0resolve-contract\0ton\0resolve-contract\0ton\0ratelance\0"
;; [what is accessible by this contract] "ton\0resolve-contract\0ton\0ratelance\0"
;; subdomain "resolve-contract"
;; subdomain_sfx "ton\0ratelance\0"
;; and we want to pass \0 further, so that next resolver has opportunity to process only one byte

;; next resolver is contract of 'resolve-contract<.ton>'
;; dns_next_resolver#ba93 resolver:MsgAddressInt = DNSRecord;
cell resolver_record = begin_cell().store_uint(0xba93, 16).store_builder(domain_nft_address).end_cell();
return (subdomain_bits - slice_bits(subdomain_sfx) - 8, resolver_record);
}
}

() recv_internal() {
return ();
}