2
Adapted from original version written by:
3
/------------------------------------------------------------------------\
4
| Created for: Telegram (Open Network) Blockchain Contest |
5
| Task 2: DNS Resolver (Automatically registering) |
6
>------------------------------------------------------------------------<
7
| Author: Oleksandr Murzin (tg: @skydev / em: alexhacker64@gmail.com) |
9
\------------------------------------------------------------------------/
10
Updated to actual DNS standard version by starlightduck in 2022
13
;;===========================================================================;;
14
;; Custom ASM instructions ;;
15
;;===========================================================================;;
17
cell udict_get_ref_(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETOPTREF";
19
;;===========================================================================;;
20
;; Utility functions ;;
21
;;===========================================================================;;
25
Root cell: [OptRef<1b+1r?>:Hashmap<PfxDict:Slice->UInt<32b>,CatTable>:domains]
26
[OptRef<1b+1r?>:Hashmap<UInt<160b>(Time|Hash128)->Slice(DomName)>:gc]
27
[UInt<32b>:stdperiod] [Gram:PPReg] [Gram:PPCell] [Gram:PPBit]
28
[UInt<32b>:lasthousekeeping]
29
<CatTable> := HashmapE 256 (~~16~~) ^DNSRecord
31
STORED DOMAIN NAME SLICE FORMAT: (#ZeroChars<7b>) (Domain name value)
32
#Zeros allows to simultaneously store, for example, com\0 and com\0google\0
33
That will be stored as \1com\0 and \2com\0google\0 (pfx tree has restricitons)
34
This will allow to resolve more specific requests to subdomains, and resort
35
to parent domain next resolver lookup if subdomain is not found
36
com\0goo\0 lookup will, for example look up \2com\0goo\0 and then
37
\1com\0goo\0 which will return \1com\0 (as per pfx tree) with -1 cat
40
(cell, cell, cell, [int, int, int, int], int, int) load_data() inline_ref {
41
slice cs = get_data().begin_parse();
43
cs~load_ref(), ;; control data
44
cs~load_dict(), ;; pfx tree: domains data and exp
45
cs~load_dict(), ;; gc auxillary with expiration and 128-bit hash slice
46
[ cs~load_uint(30), ;; length of this period of time in seconds
47
cs~load_grams(), ;; standard payment for registering a new subdomain
48
cs~load_grams(), ;; price paid for each cell (PPC)
49
cs~load_grams() ], ;; and bit (PPB)
50
cs~load_uint(32), ;; next housekeeping to be done at
51
cs~load_uint(32) ;; last housekeeping done at
55
(int, int, int, int) load_prices() inline_ref {
56
slice cs = get_data().begin_parse();
57
(cs~load_ref(), cs~load_dict(), cs~load_dict());
58
return (cs~load_uint(30), cs~load_grams(), cs~load_grams(), cs~load_grams());
61
() store_data(cell ctl, cell dd, cell gc, prices, int nhk, int lhk) impure {
62
var [sp, ppr, ppc, ppb] = prices;
64
.store_ref(ctl) ;; control data
65
.store_dict(dd) ;; domains data and exp
66
.store_dict(gc) ;; keyed expiration time and 128-bit hash slice
67
.store_uint(sp, 30) ;; standard period
68
.store_grams(ppr) ;; price per registration
69
.store_grams(ppc) ;; price per cell
70
.store_grams(ppb) ;; price per bit
71
.store_uint(nhk, 32) ;; next housekeeping
72
.store_uint(lhk, 32) ;; last housekeeping
78
() send_message(slice addr, int tag, int query_id,
79
int body, int grams, int mode) impure {
80
;; int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
81
;; src:MsgAddress -> 011000 0x18
82
var msg = begin_cell()
86
.store_uint (0, 1 + 4 + 4 + 64 + 32 + 1 + 1)
88
.store_uint (query_id, 64);
90
msg~store_uint(body, 32);
92
send_raw_message(msg.end_cell(), mode);
95
() send_error(int error_code) impure {
96
var (addr, query_id, op) = query_info;
97
return send_message(addr, error_code, query_id, op, 0, 64);
100
() send_ok(int price) impure {
101
raw_reserve(price, 4);
102
var (addr, query_id, op) = query_info;
103
return send_message(addr, 0xef6b6179, query_id, op, 0, 128);
106
() housekeeping(cell ctl, cell dd, cell gc, prices, int nhk, int lhk, int max_steps) impure {
108
if (n < max(nhk, lhk + 60)) { ;; housekeeping cooldown: 1 minute
109
;; if housekeeping was done recently, or if next housekeeping is in the future, just save
110
return store_data(ctl, dd, gc, prices, nhk, lhk);
112
;; need to do some housekeeping - maybe remove entry with
113
;; least expiration but only if it is already expired
114
;; no iterating and deleting all to not put too much gas gc
115
;; burden on any random specific user request
116
;; over time it will do the garbage collection required
117
(int mkey, _, int found?) = gc.udict_get_min?(256);
118
while (found? & max_steps) { ;; no short circuit optimization, two nested ifs
119
nhk = (mkey >> (256 - 32));
121
int key = mkey % (1 << (256 - 32));
122
(slice val, found?) = dd.udict_get?(256 - 32, key);
124
int exp = val.preload_uint(32);
126
dd~udict_delete?(256 - 32, key);
129
gc~udict_delete?(256, mkey);
130
(mkey, _, found?) = gc.udict_get_min?(256);
131
nhk = (found? ? mkey >> (256 - 32) : 0xffffffff);
137
store_data(ctl, dd, gc, prices, nhk, n);
140
int calcprice_internal(slice domain, cell data, ppc, ppb) inline_ref { ;; only for internal calcs
141
var (_, bits, refs) = compute_data_size(data, 100); ;; 100 cells max
142
bits += slice_bits(domain) * 2 + (128 + 32 + 32);
143
return ppc * (refs + 2) + ppb * bits;
146
int check_owner(cell cat_table, cell owner_info, int src_wc, int src_addr, int strict) inline_ref {
147
if (strict & cat_table.null?()) { ;; domain not found: return notf | 2^31
150
if (owner_info.null?()) { ;; no owner on this domain: no-2 (in strict mode), ok else
151
return strict & 0xee6f2d32;
153
var ERR_BAD2 = 0xe2616432;
154
slice sown = owner_info.begin_parse();
155
if (sown.slice_bits() < 16 + 3 + 8 + 256) { ;; bad owner record: bad2
158
if (sown~load_uint(16 + 3) != 0x9fd3 * 8 + 4) {
161
(int owner_wc, int owner_addr) = (sown~load_int(8), sown.preload_uint(256));
162
if ((owner_wc != src_wc) | (owner_addr != src_addr)) { ;; not owner: nown
168
;;===========================================================================;;
169
;; Internal message handler (Code 0) ;;
170
;;===========================================================================;;
173
Internal message cell structure:
175
int_msg_info$0 ihr_disabled:Bool bounce:Bool bounced:Bool
176
src:MsgAddressInt dest:MsgAddressInt
177
value:CurrencyCollection ihr_fee:Grams fwd_fee:Grams
178
created_lt:uint64 created_at:uint32
179
Internal message data structure:
180
[UInt<32b>:op] [UInt<64b>:query_id] [Ref<1r>:domain]
181
(if not prolong: [Ref<1r>:value->CatTable])
185
;; Control operations: permitted only to the owner of this smartcontract
186
() perform_ctl_op(int op, int src_wc, int src_addr, slice in_msg) impure inline_ref {
187
var (ctl, domdata, gc, prices, nhk, lhk) = load_data();
188
var cs = ctl.begin_parse();
189
if ((cs~load_int(8) != src_wc) | (cs~load_uint(256) != src_addr)) {
190
return send_error(0xee6f776e);
192
if (op == 0x43685072) { ;; ChPr = Change Prices
193
var (stdper, ppr, ppc, ppb) = (in_msg~load_uint(32), in_msg~load_grams(), in_msg~load_grams(), in_msg~load_grams());
195
;; NB: stdper == 0 -> disable new actions
196
store_data(ctl, domdata, gc, [stdper, ppr, ppc, ppb], nhk, lhk);
199
var (addr, query_id, op) = query_info;
200
if (op == 0x4344656c) { ;; CDel = destroy smart contract
201
ifnot (domdata.null?()) {
202
;; domain dictionary not empty, force gc
203
housekeeping(ctl, domdata, gc, prices, nhk, 1, -1);
205
(ctl, domdata, gc, prices, nhk, lhk) = load_data();
206
ifnot (domdata.null?()) {
207
;; domain dictionary still not empty, error
208
return send_error(0xee74656d);
210
return send_message(addr, 0xef6b6179, query_id, op, 0, 128 + 32);
212
if (op == 0x54616b65) { ;; Take = take grams from the contract
213
var amount = in_msg~load_grams();
214
return send_message(addr, 0xef6b6179, query_id, op, amount, 64);
216
return send_error(0xffffffff);
219
;; Must send at least GR$1 more for possible gas fees!
220
() recv_internal(int msg_value, cell in_msg_cell, slice in_msg) impure {
221
;; this time very interested in internal messages
222
if (in_msg.slice_bits() < 32) {
223
return (); ;; simple transfer or short
225
slice cs = in_msg_cell.begin_parse();
226
int flags = cs~load_uint(4);
228
return (); ;; bounced messages
230
slice s_addr = cs~load_msg_addr();
231
(int src_wc, int src_addr) = s_addr.parse_std_addr();
232
int op = in_msg~load_uint(32);
234
return (); ;; simple transfer with comment
237
if (in_msg.slice_bits() >= 64) {
238
query_id = in_msg~load_uint(64);
241
query_info = (s_addr, query_id, op);
243
if (op & (1 << 31)) {
244
return (); ;; an answer to our query
246
if ((op >> 24) == 0x43) {
247
;; Control operations
248
return perform_ctl_op(op, src_wc, src_addr, in_msg);
251
int qt = (op == 0x72656764) * 1 + (op == 0x70726f6c) * 2 + (op == 0x75706464) * 4 + (op == 0x676f6763) * 8;
252
ifnot (qt) { ;; unknown query, return error
253
return send_error(0xffffffff);
257
(cell ctl, cell domdata, cell gc, [int, int, int, int] prices, int nhk, int lhk) = load_data();
259
if (qt == 8) { ;; 0x676f6763 -> GO, GC! go!!!
260
;; Manual garbage collection iteration
261
int max_steps = in_msg~load_int(32); ;; -1 = infty
262
housekeeping(ctl, domdata, gc, prices, nhk, 1, max_steps); ;; forced
263
return send_error(0xef6b6179);
266
slice domain = null();
267
cell domain_cell = in_msg~load_maybe_ref();
269
if (domain_cell.null?()) {
270
int bytes = in_msg~load_uint(6);
272
domain = in_msg~load_bits(bytes * 8);
274
domain = domain_cell.begin_parse();
275
var (bits, refs) = slice_bits_refs(domain);
276
fail = (refs | ((bits - 8) & (7 - 128)));
280
;; domain must end with \0! no\0 error
281
fail = domain.slice_last(8).preload_uint(8);
284
return send_error(0xee6f5c30);
288
cell cat_table = cell owner_info = null();
289
int key = int exp = int zeros = 0;
291
repeat (tail.slice_bits() ^>> 3) {
293
int z = (tail~load_uint(8) == 0);
296
key = (string_hash(domain.skip_last_bits(tail.slice_bits())) >> 32);
297
var (val, found?) = domdata.udict_get?(256 - 32, key);
299
exp = val~load_uint(32);
300
if (exp >= n) { ;; entry not expired
301
cell cat_table = val~load_ref();
303
;; update: category length now u256 instead of i16, owner index is now 0 instead of -2
304
var (cown, ok) = cat_table.udict_get_ref?(256, 0);
313
if (zeros > 4) { ;; too much zero chars (overflow): ov\0
314
return send_error(0xef765c30);
317
;; ##########################################################################
319
int err = check_owner(cat_table, owner_info, src_wc, src_addr, qt != 1);
321
return send_error(err);
324
;; ##########################################################################
326
;; load desired data (reuse old for a "prolong" operation)
329
if (qt != 2) { ;; not a "prolong", load data dictionary
330
data = in_msg~load_ref();
331
;; basic integrity check of (client-provided) dictionary
332
ifnot (data.dict_empty?()) { ;; 1000 gas!
333
;; update: category length now u256 instead of i16, owner index is now 0 instead of -2
334
var (oinfo, ok) = data.udict_get_ref?(256, 0);
336
var cs = oinfo.begin_parse();
337
throw_unless(31, cs.slice_bits() >= 16 + 3 + 8 + 256);
338
throw_unless(31, cs.preload_uint(19) == 0x9fd3 * 8 + 4);
340
(_, _, int minok) = data.udict_get_min?(256); ;; update: category length now u256 instead of i16
341
(_, _, int maxok) = data.udict_get_max?(256); ;; update: category length now u256 instead of i16
342
throw_unless(31, minok & maxok);
349
var [stdper, ppr, ppc, ppb] = prices;
350
ifnot (stdper) { ;; smart contract disabled by owner, no new actions
351
return send_error(0xd34f4646);
354
;; compute action price
355
int price = calcprice_internal(domain, data, ppc, ppb) + (ppr & (qt != 4));
356
if (msg_value - (1 << 30) < price) { ;; gr<p: grams - GR$1 < price
357
return send_error(0xe7723c70);
360
;; load desired expiration unixtime
361
int req_expires_at = in_msg~load_uint(32);
363
;; ##########################################################################
364
if (qt == 2) { ;; 0x70726f6c -> prol | prolong domain
365
if (exp > n + stdper) { ;; does not expire soon, cannot prolong
366
return send_error(0xf365726f);
368
domdata~udict_set_builder(256 - 32, key, begin_cell().store_uint(exp + stdper, 32).store_ref(data));
370
int gckeyO = (exp << (256 - 32)) + key;
371
int gckeyN = gckeyO + (stdper << (256 - 32));
372
gc~udict_delete?(256, gckeyO); ;; delete old gc entry, add new
373
gc~udict_set_builder(256, gckeyN, begin_cell());
375
housekeeping(ctl, domdata, gc, prices, nhk, lhk, 1);
376
return send_ok(price);
379
;; ##########################################################################
380
if (qt == 1) { ;; 0x72656764 -> regd | register domain
381
ifnot (cat_table.null?()) { ;; domain already exists: return alre | 2^31
382
return send_error(0xe16c7265);
384
int expires_at = n + stdper;
385
domdata~udict_set_builder(256 - 32, key, begin_cell().store_uint(expires_at, 32).store_ref(data));
387
int gckey = (expires_at << (256 - 32)) | key;
388
gc~udict_set_builder(256, gckey, begin_cell());
390
housekeeping(ctl, domdata, gc, prices, min(nhk, expires_at), lhk, 1);
391
return send_ok(price);
394
;; ##########################################################################
395
if (qt == 4) { ;; 0x75706464 -> updd | update domain (data)
396
domdata~udict_set_builder(256 - 32, key, begin_cell().store_uint(exp, 32).store_ref(data));
397
housekeeping(ctl, domdata, gc, prices, nhk, lhk, 1);
398
return send_ok(price);
400
;; ##########################################################################
402
return (); ;; should NEVER reach this part of code!
405
;;===========================================================================;;
406
;; External message handler (Code -1) ;;
407
;;===========================================================================;;
409
() recv_external(slice in_msg) impure {
410
;; only for initialization
411
(cell ctl, cell dd, cell gc, var prices, int nhk, int lhk) = load_data();
414
return store_data(ctl, dd, gc, prices, 0xffffffff, now());
418
;;===========================================================================;;
420
;;===========================================================================;;
422
(int, cell, int, slice) dnsdictlookup(slice domain, int nowtime) inline_ref {
423
(int bits, int refs) = domain.slice_bits_refs();
424
throw_if(30, refs | (bits & 7)); ;; malformed input (~ 8n-bit)
426
;; return (0, null(), 0, null()); ;; zero-length input
427
throw(30); ;; update: throw exception for empty input
430
int domain_last_byte = domain.slice_last(8).preload_uint(8);
431
if (domain_last_byte) {
432
domain = begin_cell().store_slice(domain) ;; append zero byte
433
.store_uint(0, 8).end_cell().begin_parse();
437
return (0, null(), 8, null()); ;; zero-length input, but with zero byte
438
;; update: return 8 as resolved, but with no data
440
int domain_first_byte = domain.preload_uint(8);
441
if (domain_first_byte == 0) {
442
;; update: remove prefix \0
446
var ds = get_data().begin_parse();
447
(_, cell root) = (ds~load_ref(), ds~load_dict());
454
if (tail~load_uint(8) == 0) {
455
var key = (string_hash(domain.skip_last_bits(tail.slice_bits())) >> 32);
456
var (v, found?) = root.udict_get?(256 - 32, key);
458
if (v.preload_uint(32) >= nowtime) { ;; entry not expired
460
tail_bits = tail.slice_bits();
467
return (0, null(), 0, null()); ;; failed to find entry in subdomain dictionary
470
return (val~load_uint(32), val~load_ref(), tail_bits == 0, domain.skip_last_bits(tail_bits));
474
(int, cell) dnsresolve(slice domain, int category) method_id {
475
(int exp, cell cat_table, int exact?, slice pfx) = dnsdictlookup(domain, now());
477
return (exact?, null()); ;; update: reuse exact? to return 8 for \0
479
ifnot (exact?) { ;; incomplete subdomain found, must return next resolver (-1)
480
category = "dns_next_resolver"H; ;; 0x19f02441ee588fdb26ee24b2568dd035c3c9206e11ab979be62e55558a1d17ff
481
;; update: next resolver is now sha256("dns_next_resolver") instead of -1
484
int pfx_bits = pfx.slice_bits();
486
;; pfx.slice_bits() will contain 8m, where m is number of bytes in subdomain
487
;; COUNTING the zero byte (if structurally correct: no multiple-ZB keys)
488
;; which corresponds to 8m, m=one plus the number of bytes in the subdomain found)
490
return (pfx_bits, cat_table); ;; return cell with entire dictionary for 0
492
cell cat_found = cat_table.udict_get_ref_(256, category); ;; update: category length now u256 instead of i16
493
return (pfx_bits, cat_found);
497
;; getexpiration needs to know the current time to skip any possible expired
498
;; subdomains in the chain. it will return 0 if not found or expired.
499
int getexpirationx(slice domain, int nowtime) inline method_id {
500
(int exp, _, _, _) = dnsdictlookup(domain, nowtime);
504
int getexpiration(slice domain) method_id {
505
return getexpirationx(domain, now());
508
int getstdperiod() method_id {
509
(int stdper, _, _, _) = load_prices();
513
int getppr() method_id {
514
(_, int ppr, _, _) = load_prices();
518
int getppc() method_id {
519
(_, _, int ppc, _) = load_prices();
523
int getppb() method_id {
524
( _, _, _, int ppb) = load_prices();
528
int calcprice(slice domain, cell val) method_id { ;; only for external gets (not efficient)
529
(_, _, int ppc, int ppb) = load_prices();
530
return calcprice_internal(domain, val, ppc, ppb);
533
int calcregprice(slice domain, cell val) method_id { ;; only for external gets (not efficient)
534
(_, int ppr, int ppc, int ppb) = load_prices();
535
return ppr + calcprice_internal(domain, val, ppc, ppb);