TON DNS 解析器
介绍
TON DNS 是一个强大的工具。它不仅允许将 TON 网站/存储包分配给域名,还可以设置子域名解析。
相关链接
域名合约搜索器
子域名具有实际用途。例如,区块链浏览器目前没有提供通过名称查找域名合约的方法。让我们探索如何创建一个合约,提供查找这类域名的机会。
此合约部署在 EQDkAbAZNb4uk-6pzTPDO2s0tXZweN-2R08T2Wy6Z3qzH_Zp,并链接到 resolve-contract.ton
。要测试它,您可以在您喜欢的 TON 浏览器的地址栏中输入 <your-domain.ton>.resolve-contract.ton
,进入 TON DNS 域名合约的页面。子域名和 .t.me 域名也得到支持。
您可以尝试通过访问 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; ;; 假设 'subdomain' 不为空
if (starts_with_zero_byte) {
subdomain~load_uint(8);
if (subdomain.slice_bits() == 0) { ;; 当前合约没有自己的 DNS 记录
return (8, null());
}
}
;; 我们正在加载某个子域名
;; 支持的子域名是 "ton\\0", "me\\0t\\0" 和 "address\\0"
slice subdomain_sfx = null();
builder domain_nft_address = null();
if (subdomain.starts_with("746F6E00"s)) {
;; 我们正在解析
;; "ton" \\0 <subdomain> \\0 [subdomain_sfx]
subdomain~skip_bits(32);
;; 读取域名
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)) {
;; 解析域名的示例:
;; [初始,此合约不可访问] "ton\\0resolve-contract\\0ton\\0ratelance\\0"
;; [此合约可以访问] "ton\\0ratelance\\0"
;; subdomain "ratelance"
;; subdomain_sfx ""
;; 我们希望解析结果指向 'ratelance.ton' 合约,而不是其所有者
;; 因此我们必须回答解析已完成 + "wallet"H 是 'ratelance.ton' 合约的地址
;; 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"
;; 我们希望将 \\0 传递给下一个解析器,以便下一个解析器只处理一个字节
;; 下一个解析器是 '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() 被调用的点。 分步解释其工作方式:
- 它将子域名和类别作为输入。
- 如果开头有零字节,则跳过。
- 检查子域名是否以
"ton\0"
开头。如果是,- 跳过前32位(子域名 =
"resolve-contract\0"
) - 设置
subdomain_sfx
的值为subdomain
,并读取直到零字节的字节 - (子域名 =
"resolve-contract\0"
,subdomain_sfx =""
) - 从子域名切片的末尾裁剪零字节和 subdomain_sfx(子域名 =
"resolve-contract"
) - 使用 slice_hash 和 get_ton_dns_nft_address_by_index 函数将域名转换为合约地址。您可以在 [[Subresolvers#Appendix 1. resolve-contract.ton 的代码|附录 1]] 中看到它们。
- 跳过前32位(子域名 =
- 否则,dnsresolve() 检查子域名是否以
"address\0"
开头。如果是,它跳过该前缀并读取 base64 地址。 - 如果提供的用于解析的子域名与这些前缀都不匹配,函数通过返回
(0, null())
(零字节前缀解析无 DNS 条目)表示失败。 - 然后检查子域名后缀是否为空。空后缀表示请求已完全满足。如果后缀为空:
- dnsresolve() 为域名的 "wallet" 子部分创建一个 DNS 记录,使用它检索到的 TON 域名合约地址。
- 如果请求类别 0(所有 DNS 条目),则将记录包装在字典中并返回。
- 如果请求类别为 "wallet"H,则按原样返回记录。
- 否则,指定类别没有 DNS 条目,因此函数表示解析成功但未找到任何结果。
- 如果后缀不为空:
- 之前获得的合约地址用作下一个解析器。函数构建指向它的下一个解析器记录。
"\0ton\0stabletimer\0"
被传递给该合约:处理的位是子域名的位。
总结来说,dnsresolve() 要么:
- 将子域名完全解析为 DNS 记录
- 部分解析为解析器记录,以将解析传递给另一个合约
- 为未知子域名返回“未找到域名”的结果
实际上,base64 地址解析不起作用:如果您尝试输入 <some-address>.address.resolve-contract.ton
,您将收到一个错误,表明域名配置错误或不存在。原因是域名不区分大小写(从真实 DNS 继承的功能),因此会转换为小写,将您带到不存在的工作链的某个地址。
绑定解析器
现在子解析器合约已部署,我们需要将域名指向它,即更改域名的 dns_next_resolver
记录。我们可以通过将以下 TL-B 结构的消息发送到域名合约来实现。
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 (); } ;; 简单的资金转移
slice in_msg_full = in_msg.begin_parse();
if (in
_msg_full~load_uint(4) & 1) { return (); } ;; 弹回消息
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); ;; 不能更新“所有记录”的记录
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)) { } ;; 搜索零字节
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) { ;; 请求的内容超过 "<SUBDOMAIN>\0"
category = "dns_next_resolver"H;
}
int resolved = subdomain_bits - subdomain_suffix_bits;
if (category == 0) { ;; 请求所有类别
return (resolved, records);
}
(cell value, int found) = records.udict_get_ref?(256, category);
return (resolved, value);
}
dnsresolve
函数检查请求的子域名是否包含整数个八位字节,跳过子域名切片开头的可选零字节,然后将其分割为最高级别的域和其他部分(test\0qwerty\0
被分割为 test
和 qwerty\0
)。加载与请求的域名对应的记录字典。
如果存在非空子域名后缀,函数返回已解析的字节数和在 "dns_next_resolver"H
键下找到的下一个解析器记录。否则,函数返回已解析的字节数(即整个切片长度)和请求的记录。
可以通过更优雅地处理错误来改进此函数,但这不是绝对必需的。
附录 1. resolve-contract.ton 的代码
subresolver.fc
(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 代码的十六进制格式>}
B>boc
PUSHREF
""";
const slice tme_minter = "EQCA14o1-VWhS2efqoh_9M1b_A9DtKTuoqfmkn83AbJzwnPi"a;
cell tme_domain_code() asm """
B{<T.ME NFT 代码的十六进制格式>}
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; ;; 假设 'subdomain' 不为空
if (starts_with_zero_byte) {
subdomain~load_uint(8);
if (subdomain.slice_bits() == 0) { ;; 当前合约没有自己的 DNS 记录
return (8, null());
}
}
;; 我们正在加载某个子域名
;; 支持的子域名是 "ton\\0", "me\\0t\\0" 和 "address\\0"
slice subdomain_sfx = null();
builder domain_nft_address = null();
if (subdomain.starts_with("746F6E00"s)) {
;; 我们正在解析
;; "ton" \\0 <subdomain> \\0 [subdomain_sfx]
subdomain~skip_bits(32);
;; 读取域名
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);
;; 读取域名
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)) {
;; 解析域名的示例:
;; [初始,此合约不可访问] "ton\\0resolve-contract\\0ton\\0ratelance\\0"
;; [此合约可以访问] "ton\\0ratelance\\0"
;; subdomain "ratelance"
;; subdomain_sfx
""
;; 我们希望解析结果指向 'ratelance.ton' 合约,而不是其所有者
;; 因此我们必须回答解析已完成 + "wallet"H 是 'ratelance.ton' 合约的地址
;; 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 {
;; 解析域名的示例:
;; [初始,此合约不可访问] "ton\\0resolve-contract\\0ton\\0resolve-contract\\0ton\\0ratelance\\0"
;; [此合约可以访问] "ton\\0resolve-contract\\0ton\\0ratelance\\0"
;; subdomain "resolve-contract"
;; subdomain_sfx "ton\\0ratelance\\0"
;; 我们希望将 \\0 传递给下一个解析器,以便下一个解析器只处理一个字节
;; 下一个解析器是 '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 ();
}