mirror of
https://github.com/ton-blockchain/ton
synced 2025-02-12 19:22:37 +00:00
361 lines
13 KiB
Text
361 lines
13 KiB
Text
{-
|
|
Originally created by:
|
|
/------------------------------------------------------------------------\
|
|
| Created for: Telegram (Open Network) Blockchain Contest |
|
|
| Task 3: DNS Resolver (Manually controlled) |
|
|
>------------------------------------------------------------------------<
|
|
| Author: Oleksandr Murzin (tg: @skydev / em: alexhacker64@gmail.com) |
|
|
| October 2019 |
|
|
\------------------------------------------------------------------------/
|
|
Updated to actual DNS standard version by starlightduck in 2022
|
|
-}
|
|
|
|
;;===========================================================================;;
|
|
;; Custom ASM instructions ;;
|
|
;;===========================================================================;;
|
|
|
|
cell udict_get_ref_(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETOPTREF";
|
|
|
|
(cell, ()) pfxdict_set_ref(cell dict, int key_len, slice key, cell value) {
|
|
throw_unless(33, dict~pfxdict_set?(key_len, key, begin_cell().store_maybe_ref(value).end_cell().begin_parse()));
|
|
return (dict, ());
|
|
}
|
|
|
|
(slice, cell, slice, int) pfxdict_get_ref(cell dict, int key_len, slice key) inline_ref {
|
|
(slice pfx, slice val, slice tail, int succ) = dict.pfxdict_get?(key_len, key);
|
|
cell res = succ ? val~load_maybe_ref() : null();
|
|
return (pfx, res, tail, succ);
|
|
}
|
|
|
|
;;===========================================================================;;
|
|
;; Utility functions ;;
|
|
;;===========================================================================;;
|
|
|
|
(int, int, int, cell, cell) load_data() inline_ref {
|
|
slice cs = get_data().begin_parse();
|
|
var res = (cs~load_uint(32), cs~load_uint(64), cs~load_uint(256), cs~load_dict(), cs~load_dict());
|
|
cs.end_parse();
|
|
return res;
|
|
}
|
|
|
|
() store_data(int contract_id, int last_cleaned, int public_key, cell root, old_queries) impure {
|
|
set_data(begin_cell()
|
|
.store_uint(contract_id, 32)
|
|
.store_uint(last_cleaned, 64)
|
|
.store_uint(public_key, 256)
|
|
.store_dict(root)
|
|
.store_dict(old_queries)
|
|
.end_cell());
|
|
}
|
|
|
|
;;===========================================================================;;
|
|
;; Internal message handler (Code 0) ;;
|
|
;;===========================================================================;;
|
|
|
|
() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure {
|
|
;; not interested at all
|
|
}
|
|
|
|
;;===========================================================================;;
|
|
;; External message handler (Code -1) ;;
|
|
;;===========================================================================;;
|
|
|
|
{-
|
|
External message structure:
|
|
[Bytes<512b>:signature] [UInt<32b>:seqno] [UInt<6b>:operation]
|
|
[Either b0: inline name (<= 58-x Bytes) or b1: reference-stored name)
|
|
x depends on operation
|
|
Use of 6-bit op instead of 32-bit allows to save 4 bytes for inline name
|
|
Inline [Name] structure: [UInt<6b>:length] [Bytes<lengthB>:data]
|
|
Operations (continuation of message):
|
|
00 Contract initialization message (only if seqno = 0) (x=-)
|
|
11 VSet: set specified value to specified subdomain->category (x=2)
|
|
[UInt<256b>:category] [Name<?>:subdomain] [Cell<1r>:value]
|
|
12 VDel: delete specified subdomain->category (x=2)
|
|
[UInt<256b>:category] [Name<?>:subdomain]
|
|
21 DSet: replace entire category dictionary of domain with provided (x=0)
|
|
[Name<?>:subdomain] [Cell<1r>:new_cat_table]
|
|
22 DDel: delete entire category dictionary of specified domain (x=0)
|
|
[Name<?>:subdomain]
|
|
31 TSet: replace ENTIRE DOMAIN TABLE with the provided tree root cell (x=-)
|
|
[Cell<1r>:new_domains_table]
|
|
32 TDel: nullify ENTIRE DOMAIN TABLE (x=-)
|
|
51 OSet: replace owner public key with a new one (x=-)
|
|
[UInt<256b>:new_public_key]
|
|
-}
|
|
|
|
() after_code_upgrade(cell root, slice ops, cont old_code) impure method_id(1666);
|
|
|
|
(cell, slice) process_op(cell root, slice ops) inline_ref {
|
|
int op = ops~load_uint(6);
|
|
if (op < 10) {
|
|
ifnot (op) {
|
|
;; 00 Noop: No operation
|
|
return (root, ops);
|
|
}
|
|
if (op == 1) {
|
|
;; 01 SMsg: Send Message
|
|
var mode = ops~load_uint(8);
|
|
send_raw_message(ops~load_ref(), mode);
|
|
return (root, ops);
|
|
}
|
|
if (op == 9) {
|
|
;; 09 CodeUpgrade
|
|
var new_code = ops~load_ref();
|
|
set_code(new_code);
|
|
var old_code = get_c3();
|
|
set_c3(new_code.begin_parse().bless());
|
|
after_code_upgrade(root, ops, old_code);
|
|
throw(0);
|
|
return (root, ops);
|
|
}
|
|
throw(45);
|
|
return (root, ops);
|
|
}
|
|
int cat = 0;
|
|
if (op < 20) {
|
|
;; for operations with codes 10..19 category is required
|
|
cat = ops~load_uint(256); ;; update: category length now u256 instead of i16
|
|
}
|
|
slice name = null(); ;; any slice value
|
|
cell cat_table = null();
|
|
if (op < 30) {
|
|
;; for operations with codes 10..29 name is required
|
|
int is_name_ref = (ops~load_uint(1) == 1);
|
|
if (is_name_ref) {
|
|
;; name is stored in separate referenced cell
|
|
name = ops~load_ref().begin_parse();
|
|
} else {
|
|
;; name is stored inline
|
|
int name_len = ops~load_uint(6) * 8;
|
|
name = ops~load_bits(name_len);
|
|
}
|
|
;; at least one character not counting \0
|
|
throw_unless(38, name.slice_bits() >= 16);
|
|
;; name shall end with \0
|
|
int name_last_byte = name.slice_last(8).preload_uint(8);
|
|
throw_if(40, name_last_byte);
|
|
;; count zero separators
|
|
int zeros = 0;
|
|
slice cname = name;
|
|
repeat (cname.slice_bits() ^>> 3) {
|
|
int c = cname~load_uint(8);
|
|
zeros -= (c == 0);
|
|
}
|
|
;; throw_unless(39, zeros == 1);
|
|
name = begin_cell().store_uint(zeros, 7).store_slice(name).end_cell().begin_parse();
|
|
}
|
|
;; operation with codes 10..19 manipulate category dict
|
|
;; lets try to find it and store into a variable
|
|
;; operations with codes 20..29 replace / delete dict, no need
|
|
if (op < 20) {
|
|
;; lets resolve the name here so as not to duplicate the code
|
|
(slice pfx, cell val, slice tail, int succ) =
|
|
root.pfxdict_get_ref(1023, name);
|
|
if (succ) {
|
|
;; must match EXACTLY to prevent accident changes
|
|
throw_unless(35, tail.slice_empty?());
|
|
cat_table = val;
|
|
}
|
|
;; otherwise cat_table is null which is reasonable for actions
|
|
}
|
|
;; 11 VSet: set specified value to specified subdomain->category
|
|
if (op == 11) {
|
|
cell new_value = ops~load_maybe_ref();
|
|
cat_table~udict_set_get_ref(256, cat, new_value); ;; update: category length now u256 instead of i16
|
|
root~pfxdict_set_ref(1023, name, cat_table);
|
|
return (root, ops);
|
|
}
|
|
;; 12 VDel: delete specified subdomain->category value
|
|
if (op == 12) {
|
|
if (cat_table~udict_delete?(256, cat)) { ;; update: category length now u256 instead of i16
|
|
root~pfxdict_set_ref(1023, name, cat_table);
|
|
}
|
|
return (root, ops);
|
|
}
|
|
;; 21 DSet: replace entire category dictionary of domain with provided
|
|
if (op == 21) {
|
|
cell new_cat_table = ops~load_maybe_ref();
|
|
root~pfxdict_set_ref(1023, name, new_cat_table);
|
|
return (root, ops);
|
|
}
|
|
;; 22 DDel: delete entire category dictionary of specified domain
|
|
if (op == 22) {
|
|
root~pfxdict_delete?(1023, name);
|
|
return (root, ops);
|
|
}
|
|
;; 31 TSet: replace ENTIRE DOMAIN TABLE with the provided tree root cell
|
|
if (op == 31) {
|
|
cell new_tree_root = ops~load_maybe_ref();
|
|
;; no sanity checks cause they would cost immense gas
|
|
return (new_tree_root, ops);
|
|
}
|
|
;; 32 TDel: nullify ENTIRE DOMAIN TABLE
|
|
if (op == 32) {
|
|
return (null(), ops);
|
|
}
|
|
throw(44); ;; invalid operation
|
|
return (null(), ops);
|
|
}
|
|
|
|
cell process_ops(cell root, slice ops) inline_ref {
|
|
var stop = false;
|
|
root~touch();
|
|
ops~touch();
|
|
do {
|
|
(root, ops) = process_op(root, ops);
|
|
if (ops.slice_data_empty?()) {
|
|
if (ops.slice_refs()) {
|
|
ops = ops~load_ref().begin_parse();
|
|
} else {
|
|
stop = true;
|
|
}
|
|
}
|
|
} until (stop);
|
|
return root;
|
|
}
|
|
|
|
() recv_external(slice in_msg) impure {
|
|
;; Load data
|
|
(int contract_id, int last_cleaned, int public_key, cell root, cell old_queries) = load_data();
|
|
|
|
;; validate signature and seqno
|
|
slice signature = in_msg~load_bits(512);
|
|
int shash = slice_hash(in_msg);
|
|
var (query_contract, query_id) = (in_msg~load_uint(32), in_msg~load_uint(64));
|
|
var bound = (now() << 32);
|
|
throw_if(35, query_id < bound);
|
|
(_, var found?) = old_queries.udict_get?(64, query_id);
|
|
throw_if(32, found?);
|
|
throw_unless(34, contract_id == query_contract);
|
|
throw_unless(35, check_signature(shash, signature, public_key));
|
|
accept_message(); ;; message is signed by owner, sanity not guaranteed yet
|
|
|
|
int op = in_msg.preload_uint(6);
|
|
if (op == 51) {
|
|
in_msg~skip_bits(6);
|
|
public_key = in_msg~load_uint(256);
|
|
} else {
|
|
root = process_ops(root, in_msg);
|
|
}
|
|
|
|
bound -= (64 << 32); ;; clean up records expired more than 64 seconds ago
|
|
old_queries~udict_set_builder(64, query_id, begin_cell());
|
|
var queries = old_queries;
|
|
do {
|
|
var (old_queries', i, _, f) = old_queries.udict_delete_get_min(64);
|
|
f~touch();
|
|
if (f) {
|
|
f = (i < bound);
|
|
}
|
|
if (f) {
|
|
old_queries = old_queries';
|
|
last_cleaned = i;
|
|
}
|
|
} until (~ f);
|
|
|
|
store_data(contract_id, last_cleaned, public_key, root, old_queries);
|
|
}
|
|
|
|
() after_code_upgrade(cell root, slice ops, cont old_code) impure method_id(1666) {
|
|
}
|
|
|
|
{-
|
|
Data structure:
|
|
Root cell: [UInt<32b>:seqno] [UInt<256b>:owner_public_key]
|
|
[OptRef<1b+1r?>:Hashmap<PfxDict:Slice->CatTable>:domains]
|
|
<CatTable> := HashmapE 256 (~~16~~) ^DNSRecord
|
|
|
|
STORED DOMAIN NAME SLICE FORMAT: (#ZeroChars<7b>) (Domain name value)
|
|
#Zeros allows to simultaneously store, for example, com\0 and com\0google\0
|
|
That will be stored as \1com\0 and \2com\0google\0 (pfx tree has restricitons)
|
|
This will allow to resolve more specific requests to subdomains, and resort
|
|
to parent domain next resolver lookup if subdomain is not found
|
|
com\0goo\0 lookup will, for example look up \2com\0goo\0 and then
|
|
\1com\0goo\0 which will return \1com\0 (as per pfx tree) with -1 cat
|
|
-}
|
|
|
|
;;===========================================================================;;
|
|
;; Getter methods ;;
|
|
;;===========================================================================;;
|
|
|
|
;; Retrieve contract id (in case several contracts are managed with the same private key)
|
|
int get_contract_id() method_id {
|
|
return get_data().begin_parse().preload_uint(32);
|
|
}
|
|
|
|
int get_public_key() method_id {
|
|
var cs = get_data().begin_parse();
|
|
cs~load_uint(32 + 64);
|
|
return cs.preload_uint(256);
|
|
}
|
|
|
|
;;8m dns-record-value
|
|
(int, cell) dnsresolve(slice subdomain, int category) method_id {
|
|
int bits = subdomain.slice_bits();
|
|
ifnot (bits) {
|
|
;; return (0, null()); ;; zero-length input
|
|
throw(30); ;; update: throw exception for empty input
|
|
}
|
|
throw_if(30, bits & 7); ;; malformed input (~ 8n-bit)
|
|
|
|
int name_last_byte = subdomain.slice_last(8).preload_uint(8);
|
|
if (name_last_byte) {
|
|
subdomain = begin_cell().store_slice(subdomain) ;; append zero byte
|
|
.store_uint(0, 8).end_cell().begin_parse();
|
|
bits += 8;
|
|
}
|
|
if (bits == 8) {
|
|
return (8, null()); ;; zero-length input, but with zero byte
|
|
;; update: return 8 as resolved, but with no data
|
|
}
|
|
int name_first_byte = subdomain.preload_uint(8);
|
|
if (name_first_byte == 0) {
|
|
;; update: remove prefix \0
|
|
subdomain~load_uint(8);
|
|
bits -= 8;
|
|
}
|
|
(_, _, _, cell root, _) = load_data();
|
|
|
|
slice cname = subdomain;
|
|
int zeros = 0;
|
|
repeat (bits >> 3) {
|
|
int c = cname~load_uint(8);
|
|
zeros -= (c == 0);
|
|
}
|
|
|
|
;; can't move these declarations lower, will cause errors!
|
|
slice pfx = cname;
|
|
cell val = null();
|
|
slice tail = cname;
|
|
|
|
do {
|
|
slice pfxname = begin_cell().store_uint(zeros, 7)
|
|
.store_slice(subdomain).end_cell().begin_parse();
|
|
(pfx, val, tail, int succ) = root.pfxdict_get_ref(1023, pfxname);
|
|
zeros = succ ^ (zeros - 1); ;; break on success
|
|
} until (zeros <= 0);
|
|
|
|
ifnot (zeros) {
|
|
return (0, null()); ;; failed to find entry in prefix dictionary
|
|
}
|
|
|
|
zeros = - zeros;
|
|
|
|
ifnot (tail.slice_empty?()) { ;; if we have tail then len(pfx) < len(subdomain)
|
|
;; incomplete subdomain found, must return next resolver
|
|
category = "dns_next_resolver"H; ;; 0x19f02441ee588fdb26ee24b2568dd035c3c9206e11ab979be62e55558a1d17ff
|
|
;; update: next resolver is now sha256("dns_next_resolver") instead of -1
|
|
}
|
|
int pfx_bits = pfx.slice_bits() - 7;
|
|
cell cat_table = val;
|
|
;; pfx.slice_bits() will contain 8m, where m is number of bytes in subdomain
|
|
;; COUNTING the zero byte (if structurally correct: no multiple-ZB keys)
|
|
;; which corresponds to 8m, m=one plus the number of bytes in the subdomain found)
|
|
if (category == 0) {
|
|
return (pfx_bits, cat_table); ;; return cell with entire dictionary for 0
|
|
} else {
|
|
cell cat_found = cat_table.udict_get_ref_(256, category); ;; update: category length now u256 instead of i16
|
|
return (pfx_bits, cat_found);
|
|
}
|
|
}
|