{- Adapted from original version written by: /------------------------------------------------------------------------\ | Created for: Telegram (Open Network) Blockchain Contest | | Task 2: DNS Resolver (Automatically registering) | >------------------------------------------------------------------------< | 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"; ;;===========================================================================;; ;; Utility functions ;; ;;===========================================================================;; {- Data structure: Root cell: [OptRef<1b+1r?>:HashmapUInt<32b>,CatTable>:domains] [OptRef<1b+1r?>:Hashmap(Time|Hash128)->Slice(DomName)>:gc] [UInt<32b>:stdperiod] [Gram:PPReg] [Gram:PPCell] [Gram:PPBit] [UInt<32b>:lasthousekeeping] := 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 -} (cell, cell, cell, [int, int, int, int], int, int) load_data() inline_ref { slice cs = get_data().begin_parse(); return ( cs~load_ref(), ;; control data cs~load_dict(), ;; pfx tree: domains data and exp cs~load_dict(), ;; gc auxillary with expiration and 128-bit hash slice [ cs~load_uint(30), ;; length of this period of time in seconds cs~load_grams(), ;; standard payment for registering a new subdomain cs~load_grams(), ;; price paid for each cell (PPC) cs~load_grams() ], ;; and bit (PPB) cs~load_uint(32), ;; next housekeeping to be done at cs~load_uint(32) ;; last housekeeping done at ); } (int, int, int, int) load_prices() inline_ref { slice cs = get_data().begin_parse(); (cs~load_ref(), cs~load_dict(), cs~load_dict()); return (cs~load_uint(30), cs~load_grams(), cs~load_grams(), cs~load_grams()); } () store_data(cell ctl, cell dd, cell gc, prices, int nhk, int lhk) impure { var [sp, ppr, ppc, ppb] = prices; set_data(begin_cell() .store_ref(ctl) ;; control data .store_dict(dd) ;; domains data and exp .store_dict(gc) ;; keyed expiration time and 128-bit hash slice .store_uint(sp, 30) ;; standard period .store_grams(ppr) ;; price per registration .store_grams(ppc) ;; price per cell .store_grams(ppb) ;; price per bit .store_uint(nhk, 32) ;; next housekeeping .store_uint(lhk, 32) ;; last housekeeping .end_cell()); } global var query_info; () send_message(slice addr, int tag, int query_id, int body, int grams, int mode) impure { ;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool ;; src:MsgAddress -> 011000 0x18 var msg = begin_cell() .store_uint (0x18, 6) .store_slice(addr) .store_grams(grams) .store_uint (0, 1 + 4 + 4 + 64 + 32 + 1 + 1) .store_uint (tag, 32) .store_uint (query_id, 64); if (body >= 0) { msg~store_uint(body, 32); } send_raw_message(msg.end_cell(), mode); } () send_error(int error_code) impure { var (addr, query_id, op) = query_info; return send_message(addr, error_code, query_id, op, 0, 64); } () send_ok(int price) impure { raw_reserve(price, 4); var (addr, query_id, op) = query_info; return send_message(addr, 0xef6b6179, query_id, op, 0, 128); } () housekeeping(cell ctl, cell dd, cell gc, prices, int nhk, int lhk, int max_steps) impure { int n = now(); if (n < max(nhk, lhk + 60)) { ;; housekeeping cooldown: 1 minute ;; if housekeeping was done recently, or if next housekeeping is in the future, just save return store_data(ctl, dd, gc, prices, nhk, lhk); } ;; need to do some housekeeping - maybe remove entry with ;; least expiration but only if it is already expired ;; no iterating and deleting all to not put too much gas gc ;; burden on any random specific user request ;; over time it will do the garbage collection required (int mkey, _, int found?) = gc.udict_get_min?(256); while (found? & max_steps) { ;; no short circuit optimization, two nested ifs nhk = (mkey >> (256 - 32)); if (nhk < n) { int key = mkey % (1 << (256 - 32)); (slice val, found?) = dd.udict_get?(256 - 32, key); if (found?) { int exp = val.preload_uint(32); if (exp <= n) { dd~udict_delete?(256 - 32, key); } } gc~udict_delete?(256, mkey); (mkey, _, found?) = gc.udict_get_min?(256); nhk = (found? ? mkey >> (256 - 32) : 0xffffffff); max_steps -= 1; } else { found? = false; } } store_data(ctl, dd, gc, prices, nhk, n); } int calcprice_internal(slice domain, cell data, ppc, ppb) inline_ref { ;; only for internal calcs var (_, bits, refs) = compute_data_size(data, 100); ;; 100 cells max bits += slice_bits(domain) * 2 + (128 + 32 + 32); return ppc * (refs + 2) + ppb * bits; } int check_owner(cell cat_table, cell owner_info, int src_wc, int src_addr, int strict) inline_ref { if (strict & cat_table.null?()) { ;; domain not found: return notf | 2^31 return 0xee6f7466; } if (owner_info.null?()) { ;; no owner on this domain: no-2 (in strict mode), ok else return strict & 0xee6f2d32; } var ERR_BAD2 = 0xe2616432; slice sown = owner_info.begin_parse(); if (sown.slice_bits() < 16 + 3 + 8 + 256) { ;; bad owner record: bad2 return ERR_BAD2; } if (sown~load_uint(16 + 3) != 0x9fd3 * 8 + 4) { return ERR_BAD2; } (int owner_wc, int owner_addr) = (sown~load_int(8), sown.preload_uint(256)); if ((owner_wc != src_wc) | (owner_addr != src_addr)) { ;; not owner: nown return 0xee6f776e; } return 0; ;; ok } ;;===========================================================================;; ;; Internal message handler (Code 0) ;; ;;===========================================================================;; {- Internal message cell structure: 8 4 2 1 int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool src:MsgAddressInt dest:MsgAddressInt value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams created_lt:uint64 created_at:uint32 Internal message data structure: [UInt<32b>:op] [UInt<64b>:query_id] [Ref<1r>:domain] (if not prolong: [Ref<1r>:value->CatTable]) -} ;; Control operations: permitted only to the owner of this smartcontract () perform_ctl_op(int op, int src_wc, int src_addr, slice in_msg) impure inline_ref { var (ctl, domdata, gc, prices, nhk, lhk) = load_data(); var cs = ctl.begin_parse(); if ((cs~load_int(8) != src_wc) | (cs~load_uint(256) != src_addr)) { return send_error(0xee6f776e); } if (op == 0x43685072) { ;; ChPr = Change Prices var (stdper, ppr, ppc, ppb) = (in_msg~load_uint(32), in_msg~load_grams(), in_msg~load_grams(), in_msg~load_grams()); in_msg.end_parse(); ;; NB: stdper == 0 -> disable new actions store_data(ctl, domdata, gc, [stdper, ppr, ppc, ppb], nhk, lhk); return send_ok(0); } var (addr, query_id, op) = query_info; if (op == 0x4344656c) { ;; CDel = destroy smart contract ifnot (domdata.null?()) { ;; domain dictionary not empty, force gc housekeeping(ctl, domdata, gc, prices, nhk, 1, -1); } (ctl, domdata, gc, prices, nhk, lhk) = load_data(); ifnot (domdata.null?()) { ;; domain dictionary still not empty, error return send_error(0xee74656d); } return send_message(addr, 0xef6b6179, query_id, op, 0, 128 + 32); } if (op == 0x54616b65) { ;; Take = take grams from the contract var amount = in_msg~load_grams(); return send_message(addr, 0xef6b6179, query_id, op, amount, 64); } return send_error(0xffffffff); } ;; Must send at least GR$1 more for possible gas fees! () recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure { ;; this time very interested in internal messages if (in_msg.slice_bits() < 32) { return (); ;; simple transfer or short } slice cs = in_msg_cell.begin_parse(); int flags = cs~load_uint(4); if (flags & 1) { return (); ;; bounced messages } slice s_addr = cs~load_msg_addr(); (int src_wc, int src_addr) = s_addr.parse_std_addr(); int op = in_msg~load_uint(32); ifnot (op) { return (); ;; simple transfer with comment } int query_id = 0; if (in_msg.slice_bits() >= 64) { query_id = in_msg~load_uint(64); } query_info = (s_addr, query_id, op); if (op & (1 << 31)) { return (); ;; an answer to our query } if ((op >> 24) == 0x43) { ;; Control operations return perform_ctl_op(op, src_wc, src_addr, in_msg); } int qt = (op == 0x72656764) * 1 + (op == 0x70726f6c) * 2 + (op == 0x75706464) * 4 + (op == 0x676f6763) * 8; ifnot (qt) { ;; unknown query, return error return send_error(0xffffffff); } qt = - qt; (cell ctl, cell domdata, cell gc, [int, int, int, int] prices, int nhk, int lhk) = load_data(); if (qt == 8) { ;; 0x676f6763 -> GO, GC! go!!! ;; Manual garbage collection iteration int max_steps = in_msg~load_int(32); ;; -1 = infty housekeeping(ctl, domdata, gc, prices, nhk, 1, max_steps); ;; forced return send_error(0xef6b6179); } slice domain = null(); cell domain_cell = in_msg~load_maybe_ref(); int fail = 0; if (domain_cell.null?()) { int bytes = in_msg~load_uint(6); fail = (bytes == 0); domain = in_msg~load_bits(bytes * 8); } else { domain = domain_cell.begin_parse(); var (bits, refs) = slice_bits_refs(domain); fail = (refs | ((bits - 8) & (7 - 128))); } ifnot (fail) { ;; domain must end with \0! no\0 error fail = domain.slice_last(8).preload_uint(8); } if (fail) { return send_error(0xee6f5c30); } int n = now(); cell cat_table = cell owner_info = null(); int key = int exp = int zeros = 0; slice tail = domain; repeat (tail.slice_bits() ^>> 3) { cat_table = null(); int z = (tail~load_uint(8) == 0); zeros -= z; if (z) { key = (string_hash(domain.skip_last_bits(tail.slice_bits())) >> 32); var (val, found?) = domdata.udict_get?(256 - 32, key); if (found?) { exp = val~load_uint(32); if (exp >= n) { ;; entry not expired cell cat_table = val~load_ref(); val.end_parse(); ;; update: category length now u256 instead of i16, owner index is now 0 instead of -2 var (cown, ok) = cat_table.udict_get_ref?(256, 0); if (ok) { owner_info = cown; } } } } } if (zeros > 4) { ;; too much zero chars (overflow): ov\0 return send_error(0xef765c30); } ;; ########################################################################## int err = check_owner(cat_table, owner_info, src_wc, src_addr, qt != 1); if (err) { return send_error(err); } ;; ########################################################################## ;; load desired data (reuse old for a "prolong" operation) cell data = null(); if (qt != 2) { ;; not a "prolong", load data dictionary data = in_msg~load_ref(); ;; basic integrity check of (client-provided) dictionary ifnot (data.dict_empty?()) { ;; 1000 gas! ;; update: category length now u256 instead of i16, owner index is now 0 instead of -2 var (oinfo, ok) = data.udict_get_ref?(256, 0); if (ok) { var cs = oinfo.begin_parse(); throw_unless(31, cs.slice_bits() >= 16 + 3 + 8 + 256); throw_unless(31, cs.preload_uint(19) == 0x9fd3 * 8 + 4); } (_, _, int minok) = data.udict_get_min?(256); ;; update: category length now u256 instead of i16 (_, _, int maxok) = data.udict_get_max?(256); ;; update: category length now u256 instead of i16 throw_unless(31, minok & maxok); } } else { data = cat_table; } ;; load prices var [stdper, ppr, ppc, ppb] = prices; ifnot (stdper) { ;; smart contract disabled by owner, no new actions return send_error(0xd34f4646); } ;; compute action price int price = calcprice_internal(domain, data, ppc, ppb) + (ppr & (qt != 4)); if (msg_value - (1 << 30) < price) { ;; gr prol | prolong domain if (exp > n + stdper) { ;; does not expire soon, cannot prolong return send_error(0xf365726f); } domdata~udict_set_builder(256 - 32, key, begin_cell().store_uint(exp + stdper, 32).store_ref(data)); int gckeyO = (exp << (256 - 32)) + key; int gckeyN = gckeyO + (stdper << (256 - 32)); gc~udict_delete?(256, gckeyO); ;; delete old gc entry, add new gc~udict_set_builder(256, gckeyN, begin_cell()); housekeeping(ctl, domdata, gc, prices, nhk, lhk, 1); return send_ok(price); } ;; ########################################################################## if (qt == 1) { ;; 0x72656764 -> regd | register domain ifnot (cat_table.null?()) { ;; domain already exists: return alre | 2^31 return send_error(0xe16c7265); } int expires_at = n + stdper; domdata~udict_set_builder(256 - 32, key, begin_cell().store_uint(expires_at, 32).store_ref(data)); int gckey = (expires_at << (256 - 32)) | key; gc~udict_set_builder(256, gckey, begin_cell()); housekeeping(ctl, domdata, gc, prices, min(nhk, expires_at), lhk, 1); return send_ok(price); } ;; ########################################################################## if (qt == 4) { ;; 0x75706464 -> updd | update domain (data) domdata~udict_set_builder(256 - 32, key, begin_cell().store_uint(exp, 32).store_ref(data)); housekeeping(ctl, domdata, gc, prices, nhk, lhk, 1); return send_ok(price); } ;; ########################################################################## return (); ;; should NEVER reach this part of code! } ;;===========================================================================;; ;; External message handler (Code -1) ;; ;;===========================================================================;; () recv_external(slice in_msg) impure { ;; only for initialization (cell ctl, cell dd, cell gc, var prices, int nhk, int lhk) = load_data(); ifnot (lhk) { accept_message(); return store_data(ctl, dd, gc, prices, 0xffffffff, now()); } } ;;===========================================================================;; ;; Getter methods ;; ;;===========================================================================;; (int, cell, int, slice) dnsdictlookup(slice domain, int nowtime) inline_ref { (int bits, int refs) = domain.slice_bits_refs(); throw_if(30, refs | (bits & 7)); ;; malformed input (~ 8n-bit) ifnot (bits) { ;; return (0, null(), 0, null()); ;; zero-length input throw(30); ;; update: throw exception for empty input } int domain_last_byte = domain.slice_last(8).preload_uint(8); if (domain_last_byte) { domain = begin_cell().store_slice(domain) ;; append zero byte .store_uint(0, 8).end_cell().begin_parse(); bits += 8; } if (bits == 8) { return (0, null(), 8, null()); ;; zero-length input, but with zero byte ;; update: return 8 as resolved, but with no data } int domain_first_byte = domain.preload_uint(8); if (domain_first_byte == 0) { ;; update: remove prefix \0 domain~load_uint(8); bits -= 8; } var ds = get_data().begin_parse(); (_, cell root) = (ds~load_ref(), ds~load_dict()); slice val = null(); int tail_bits = -1; slice tail = domain; repeat (bits >> 3) { if (tail~load_uint(8) == 0) { var key = (string_hash(domain.skip_last_bits(tail.slice_bits())) >> 32); var (v, found?) = root.udict_get?(256 - 32, key); if (found?) { if (v.preload_uint(32) >= nowtime) { ;; entry not expired val = v; tail_bits = tail.slice_bits(); } } } } if (val.null?()) { return (0, null(), 0, null()); ;; failed to find entry in subdomain dictionary } return (val~load_uint(32), val~load_ref(), tail_bits == 0, domain.skip_last_bits(tail_bits)); } ;;8m dns-record-value (int, cell) dnsresolve(slice domain, int category) method_id { (int exp, cell cat_table, int exact?, slice pfx) = dnsdictlookup(domain, now()); ifnot (exp) { return (exact?, null()); ;; update: reuse exact? to return 8 for \0 } ifnot (exact?) { ;; incomplete subdomain found, must return next resolver (-1) category = "dns_next_resolver"H; ;; 0x19f02441ee588fdb26ee24b2568dd035c3c9206e11ab979be62e55558a1d17ff ;; update: next resolver is now sha256("dns_next_resolver") instead of -1 } int pfx_bits = pfx.slice_bits(); ;; 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) ifnot (category) { 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); } } ;; getexpiration needs to know the current time to skip any possible expired ;; subdomains in the chain. it will return 0 if not found or expired. int getexpirationx(slice domain, int nowtime) inline method_id { (int exp, _, _, _) = dnsdictlookup(domain, nowtime); return exp; } int getexpiration(slice domain) method_id { return getexpirationx(domain, now()); } int getstdperiod() method_id { (int stdper, _, _, _) = load_prices(); return stdper; } int getppr() method_id { (_, int ppr, _, _) = load_prices(); return ppr; } int getppc() method_id { (_, _, int ppc, _) = load_prices(); return ppc; } int getppb() method_id { ( _, _, _, int ppb) = load_prices(); return ppb; } int calcprice(slice domain, cell val) method_id { ;; only for external gets (not efficient) (_, _, int ppc, int ppb) = load_prices(); return calcprice_internal(domain, val, ppc, ppb); } int calcregprice(slice domain, cell val) method_id { ;; only for external gets (not efficient) (_, int ppr, int ppc, int ppb) = load_prices(); return ppr + calcprice_internal(domain, val, ppc, ppb); }