mirror of
https://github.com/ton-blockchain/ton
synced 2025-02-13 03:32:22 +00:00
474 lines
16 KiB
Text
474 lines
16 KiB
Text
{-
|
|
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 |
|
|
\------------------------------------------------------------------------/
|
|
-}
|
|
|
|
;;===========================================================================;;
|
|
;; Utility functions ;;
|
|
;;===========================================================================;;
|
|
|
|
{-
|
|
Data structure:
|
|
Root cell: [OptRef<1b+1r?>:Hashmap<PfxDict:Slice->UInt<32b>,CatTable>:domains]
|
|
[OptRef<1b+1r?>:Hashmap<UInt<64b>(Time|Hash32)->Slice(DomName)>:gc]
|
|
[UInt<32b>:stdperiod] [Gram:PPReg] [Gram:PPCell] [Gram:PPBit]
|
|
[UInt<32b>:lasthousekeeping]
|
|
<CatTable> := HashmapE 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, [int, int, int, int], int, int) load_data() inline_ref {
|
|
slice cs = get_data().begin_parse();
|
|
return (
|
|
cs~load_dict(), ;; pfx tree: domains data and exp
|
|
cs~load_dict(), ;; gc auxillary with expiry and 32 bit hash slice
|
|
[ cs~load_uint(32), ;; 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_dict(), cs~load_dict());
|
|
return (cs~load_uint(32), cs~load_grams(), cs~load_grams(), cs~load_grams());
|
|
}
|
|
|
|
() store_data(cell dd, cell gc, prices, int nhk, int lhk) impure {
|
|
var [sp, ppr, ppc, ppb] = prices;
|
|
set_data(begin_cell()
|
|
.store_dict(dd) ;; domains data and exp
|
|
.store_dict(gc) ;; keyed expiration time and 32 bit hash slice
|
|
.store_int(sp, 32) ;; 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 dd, cell gc, prices, int nhk, int lhk) 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(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, cell name, int found?) = gc.udict_get_min_ref?(64);
|
|
if (found?) { ;; no short circuit optimization, two nested ifs
|
|
nhk = (mkey >> 32);
|
|
if (nhk < n) {
|
|
slice sname = name.begin_parse();
|
|
(_, slice val, _, found?) = dd.pfxdict_get?(1023, sname);
|
|
if (found?) {
|
|
int exp = val.preload_uint(32);
|
|
if (exp <= n) {
|
|
dd~pfxdict_delete?(1023, sname);
|
|
}
|
|
}
|
|
gc~udict_delete?(64, mkey);
|
|
(mkey, _, found?) = gc.udict_get_min_ref?(64);
|
|
nhk = (found? ? mkey >> 32 : 0xffffffff);
|
|
}
|
|
}
|
|
store_data(dd, gc, prices, nhk, n);
|
|
}
|
|
|
|
int _calcprice(cell data, ppc, ppb) inline_ref { ;; only for internal calcs
|
|
var (_, bits, refs) = compute_data_size(data, 100); ;; 100 cells max
|
|
return ppc * refs + ppb * bits;
|
|
}
|
|
|
|
int check_owner(cell cat_table, int src_wc, int src_addr) inline_ref {
|
|
if (cat_table.null?()) { ;; domain not found: return notf | 2^31
|
|
return 0xee6f7466;
|
|
}
|
|
cell cown = cat_table.idict_get_ref(16, -2);
|
|
if (cown.null?()) { ;; no owner on this domain: no-2
|
|
return 0xee6f2d32;
|
|
}
|
|
var ERR_BAD2 = 0xe2616432;
|
|
slice sown = cown.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>:name]
|
|
(if not prolong: [Ref<1r>:value->CatTable])
|
|
|
|
-}
|
|
|
|
;; Must send at least GR$1 more for possible gas fees!
|
|
() recv_internal(int ct_bal, 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
|
|
}
|
|
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 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
|
|
housekeeping(domdata, gc, prices, nhk, 1); ;; forced
|
|
return send_error(0xef6b6179);
|
|
}
|
|
|
|
slice name = null();
|
|
cell name_cell = in_msg~load_maybe_ref();
|
|
if (name_cell.null?()) {
|
|
int bytes = in_msg~load_uint(6);
|
|
name = in_msg~load_bits(bytes * 8);
|
|
} else {
|
|
name = name_cell.begin_parse();
|
|
}
|
|
|
|
(_, int name_last_byte) = name.slice_last(8).load_uint(8);
|
|
if (name_last_byte != 0) { ;; name must end with \0! no\0 error
|
|
return send_error(0xee6f5c30);
|
|
}
|
|
|
|
int zeros = 0;
|
|
slice cname = name;
|
|
repeat (cname.slice_bits() ^>> 3) {
|
|
int c = cname~load_uint(8);
|
|
zeros -= (c == 0);
|
|
}
|
|
|
|
;; if (zeros != 1) { ;; too much zero chars (overflow): ov\0
|
|
;; return send_error(0xef765c30); }
|
|
|
|
name = begin_cell().store_uint(zeros, 7).store_slice(name).end_cell().begin_parse();
|
|
|
|
(slice pfx, slice val, slice tail, int found?) = domdata.pfxdict_get?(1023, name);
|
|
int n = now();
|
|
cell cat_table = null();
|
|
int exp = 0;
|
|
|
|
if (found?) {
|
|
exp = val~load_uint(32);
|
|
if (n > exp) { ;; expired domains behave as not registered
|
|
found? = false;
|
|
} else {
|
|
cat_table = val.preload_ref();
|
|
}
|
|
}
|
|
|
|
;; ##########################################################################
|
|
|
|
int err = 0;
|
|
if (qt != 1) { ;; not a "register", check that domain exists and is controlled by correct smc
|
|
err = check_owner(cat_table, src_wc, src_addr);
|
|
}
|
|
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!
|
|
(int dmin, _, int minok) = idict_get_min?(data, 16);
|
|
(int dmax, _, int maxok) = idict_get_max?(data, 16);
|
|
throw_unless(31, minok & maxok & (dmin <= dmax));
|
|
}
|
|
} else {
|
|
data = cat_table;
|
|
}
|
|
|
|
;; compute action price
|
|
var [stdper, ppr, ppc, ppb] = prices;
|
|
int price = _calcprice(data, ppc, ppb) + (ppr & (qt != 4));
|
|
if (msg_value - (1 << 30) < price) { ;; gr<p: grams - GR$1 < price
|
|
return send_error(0xe7723c70);
|
|
}
|
|
|
|
;; load desired expiration unixtime
|
|
int req_expires_at = in_msg~load_uint(32);
|
|
|
|
;; ##########################################################################
|
|
if (qt == 2) { ;; 0x70726f6c -> prol | prolong domain
|
|
slice value = begin_cell().store_uint(exp + stdper, 32).store_ref(data).end_cell().begin_parse();
|
|
|
|
ifnot (domdata~pfxdict_set?(1023, name, value)) { ;; Set ERR | 2^31
|
|
return send_error(0xf3657272);
|
|
}
|
|
|
|
int sh_low = name.slice_hash() & ((1 << 32) - 1);
|
|
int gckeyO = (exp << 32) + sh_low;
|
|
int gckeyN = gckeyO + (stdper << 32);
|
|
gc~udict_delete?(64, gckeyO); ;; delete old gc entry, add new
|
|
gc~udict_set_ref(64, gckeyN, begin_cell().store_slice(name).end_cell());
|
|
|
|
housekeeping(domdata, gc, prices, nhk, lhk);
|
|
return send_ok(price);
|
|
}
|
|
|
|
;; ##########################################################################
|
|
if (qt == 1) { ;; 0x72656764 -> regd | register domain
|
|
if (found?) { ;; domain already exists: return alre | 2^31
|
|
return send_error(0xe16c7265);
|
|
}
|
|
int expires_at = n + stdper;
|
|
slice value = begin_cell().store_uint(expires_at, 32).store_ref(data).end_cell().begin_parse();
|
|
ifnot (domdata~pfxdict_set?(1023, name, value)) { ;; Set ERR | 2^31
|
|
return send_error(0xf3657272);
|
|
}
|
|
int gckey = (expires_at << 32) | (name.slice_hash() & ((1 << 32) - 1));
|
|
gc~udict_set_ref(64, gckey, begin_cell().store_slice(name).end_cell());
|
|
;; using ref requires additional cell, but using value (DICTUSET) may
|
|
;; cause problems with very long names or complex dictionaries
|
|
housekeeping(domdata, gc, prices, min(nhk, expires_at), lhk);
|
|
return send_ok(price);
|
|
}
|
|
|
|
;; ##########################################################################
|
|
if (qt == 4) { ;; 0x75706464 -> updd | update domain (data)
|
|
slice value = begin_cell().store_uint(exp, 32).store_ref(data).end_cell().begin_parse();
|
|
|
|
ifnot (domdata~pfxdict_set?(1023, name, value)) { ;; Set ERR | 2^31
|
|
return send_error(0xf3657272);
|
|
}
|
|
;; no need to update gc here
|
|
housekeeping(domdata, gc, prices, nhk, lhk);
|
|
return send_ok(price);
|
|
}
|
|
;; ##########################################################################
|
|
|
|
return (); ;; should NEVER reach this part of code!
|
|
}
|
|
|
|
;;===========================================================================;;
|
|
;; External message handler (Code -1) ;;
|
|
;;===========================================================================;;
|
|
|
|
() recv_external(slice in_msg) impure {
|
|
;; not interested at all! but need to init!
|
|
(cell dd, cell gc, var prices, int nhk, int lhk) = load_data();
|
|
ifnot (lhk) {
|
|
accept_message();
|
|
store_data(dd, gc, prices, 0, now());
|
|
}
|
|
}
|
|
|
|
;;===========================================================================;;
|
|
;; Getter methods ;;
|
|
;;===========================================================================;;
|
|
|
|
(int, cell, int, slice) dnsdictlookup(slice subdomain, int nowtime) inline_ref {
|
|
int bits = subdomain.slice_bits();
|
|
ifnot (bits) {
|
|
return (0, null(), 0, null()); ;; zero-length 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 (0, null(), 0, null()); ;; zero-length input, but with zero byte
|
|
}
|
|
(_, cell root) = get_data().begin_parse().load_dict();
|
|
|
|
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;
|
|
slice val = null();
|
|
slice tail = cname;
|
|
int exp = 0;
|
|
|
|
do {
|
|
slice pfxname = begin_cell().store_uint(zeros, 7)
|
|
.store_slice(subdomain).end_cell().begin_parse();
|
|
(pfx, val, tail, int succ) = root.pfxdict_get?(1023, pfxname);
|
|
if (succ) {
|
|
int exp = val~load_uint(32);
|
|
if (nowtime > exp) { ;; entry expired, skip
|
|
succ = false;
|
|
}
|
|
}
|
|
zeros = succ ^ (zeros - 1); ;; break on success
|
|
} until (zeros <= 0);
|
|
|
|
ifnot (zeros) {
|
|
return (0, null(), 0, null()); ;; failed to find entry in prefix dictionary
|
|
}
|
|
|
|
zeros = - zeros;
|
|
return (exp, val.preload_ref(), tail.slice_empty?(), pfx);
|
|
}
|
|
|
|
;;8m dns-record-value
|
|
(int, cell) dnsresolve(slice subdomain, int category) method_id {
|
|
(int exp, cell cat_table, int exact?, slice pfx) = dnsdictlookup(subdomain, now());
|
|
ifnot (exp) {
|
|
return (0, null());
|
|
}
|
|
ifnot (exact?) { ;; incomplete subdomain found, must return next resolver (-1)
|
|
category = -1;
|
|
}
|
|
|
|
int pfx_bits = pfx.slice_bits() - 7;
|
|
|
|
;; 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.idict_get_ref(16, category);
|
|
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 subdomain, int nowtime) inline method_id {
|
|
(int exp, _, _, _) = dnsdictlookup(subdomain, nowtime);
|
|
return exp;
|
|
}
|
|
|
|
int getexpiration(slice subdomain) method_id {
|
|
return getexpirationx(subdomain, 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(cell val) method_id { ;; only for external gets (not efficient)
|
|
(_, _, int ppc, int ppb) = load_prices();
|
|
return _calcprice(val, ppc, ppb);
|
|
}
|
|
|
|
int calcregprice(cell val) method_id { ;; only for external gets (not efficient)
|
|
(_, int ppr, int ppc, int ppb) = load_prices();
|
|
return ppr + _calcprice(val, ppc, ppb);
|
|
}
|