跳到主要内容

TON DNS 解析器

介绍

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

相关链接

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

域名合约搜索器

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

信息

此合约部署在 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() 被调用的点。 分步解释其工作方式:

  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 (); } ;; 简单的资金转移

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 被分割为 testqwerty\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 ();
}