Skip to main content

TON DNS resolvers

Introduction

TON DNS is a powerful tool. It allows not only to assign TON Sites/Storage bags to domains, but also to set up subdomain resolving.

  1. TON smart-contract address system
  2. TEP-0081 - TON DNS Standard
  3. Source code of .ton DNS collection
  4. Source code of .t.me DNS collection
  5. Domain contracts searcher
  6. Simple subdomain manager code

Domain contracts searcher

Subdomains have practical use. For example, blockchain explorers don't currently provide way to find domain contract by its name. Let's explore how to create contract that gives an opportunity to find such domains.

info

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.

You can attempt to see the resolver code by going to resolve-contract.ton.resolve-contract.ton. Unfortunately, that will not show you the subresolver (that is a different smart-contract), you will see the page of domain contract itself.

dnsresolve() code

Some repeated parts are omitted.

(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);
}
}

Explanation of dnsresolve()

  • User requests "stabletimer.ton.resolve-contract.ton".
  • Application translates that into "\0ton\0resolve-contract\0ton\0stabletimer\0" (the first zero byte is optional).
  • Root DNS resolver directs the request to TON DNS collection, remaining part is "\0resolve-contract\0ton\0stabletimer\0".
  • TON DNS collection delegates the request to the specific domain, leaving "\0ton\0stabletimer\0".
  • .TON DNS domain contract passes resolution to subresolver specified by editor, subdomain is "ton\0stabletimer\0".

This is the point where dnsresolve() is invoked. A step-by-step breakdown of how it works:

  1. It takes the subdomain and category as input.
  2. If there is zero byte at the beginning, it is skipped.
  3. It checks if the subdomain starts with "ton\0". If so,
    1. it skips the first 32 bits (subdomain = "resolve-contract\0")
    2. subdomain_sfx value is set to subdomain, and the function reads the bytes until zero byte
    3. (subdomain = "resolve-contract\0", subdomain_sfx = "")
    4. Zero byte and subdomain_sfx are trimmed from the end of subdomain slice (subdomain = "resolve-contract")
    5. Functions slice_hash and get_ton_dns_nft_address_by_index is used to convert domain name to the contract address. You can see them in [[Subresolvers#Appendix 1. Code of resolve-contract.ton|Appendix 1]].
  4. Otherwise, dnsresolve() checks if the subdomain starts with "address\0". If so, it skips that prefix and reads base64 address.
  5. If provided subdomain for resolution did not match any of these prefixes, function indicates failure by returning (0, null()) (zero bytes prefix resolved with no DNS entries).
  6. It then checks if the subdomain suffix is empty. An empty suffix indicates the request was fully satisfied. If the suffix is empty:
    1. dnsresolve() creates a DNS record for the "wallet" subsection of the domain, using the TON Domain contract address it retrieved.
    2. If category 0 (all DNS entries) is requested, the record is wrapped in the dictionary and returned.
    3. If category "wallet"H is requested, the record is returned as is.
    4. Otherwise, there is no DNS entry for the specified category, so function indicates that resolution was successful but didn't find any results.
  7. If the suffix is not empty:
    1. The contract address obtained before is used as the next resolver. The function builds the next resolver record pointing at it.
    2. "\0ton\0stabletimer\0" is passed further to that contract: processed bits are bits of subdomain.

So in summary, dnsresolve() either:

  • Fully resolves the subdomain to a DNS record
  • Partially resolves it to a resolver record to pass resolution to another contract
  • Returns a "domain not found" result for unknown subdomains
danger

Actually, base64 addresses parsing does not work: if you attempt to enter <some-address>.address.resolve-contract.ton, you will get an error saying that domain is misconfigured or does not exist. The reason for that is that domain names are case-insensitive (feature inherited from real DNS) and thus are converted to lowercase, taking you to some address of non-existent workchain.

Binding the resolver

Now that the subresolver contract is deployed, we need to point the domain to it, that is, to change domain dns_next_resolver record. We may do so by sending message with the following TL-B structure to the domain contract.

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

Creating own subdomains manager

Subdomains can be useful for regular users - for example, to link several projects to a single domain, or to link to friends' wallets.

Contract data

We need to store owner's address and the domain->record hash->record value dictionary in the contract data.

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());
}

Processing records update

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();
}
}

We check that the incoming message contains some request, is not bounced, comes from the owner and that the request is op::update_record.

Then we load domain name from the message. We can't store domains in dictionary as-is: they may have different lengths, but TVM non-prefix dictionaries can only contain keys of equal length. Thus, we calculate string_hash(domain) - SHA-256 of domain name; domain name is guaranteed to have an integer number of octets so that works.

After that, we update the record for the specified domain and save new data into the contract storage.

Resolving domains

(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);
}

The dnsresolve function checks if the requested subdomain contains integer number of octets, skips optional zero byte in the beginning of the subdomain slice, then splits it into the topmost-level domain and everything other (test\0qwerty\0 gets split into test and qwerty\0). The record dictionary corresponding to the requested domain is loaded.

If there is a non-empty subdomain suffix, the function returns the number of bytes resolved and the next resolver record, found at "dns_next_resolver"H key. Otherwise, the function returns the number of bytes resolved (that is, the full slice length) and the record requested.

There is a way to improve this function by handling errors more gracefully, but it is not strictly required.

Appendix 1. Code of 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 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 ();
}