/* This file is part of TON Blockchain Library. TON Blockchain Library is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. TON Blockchain Library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with TON Blockchain Library. If not, see . Copyright 2019-2020 Telegram Systems LLP */ #include "ManualDns.h" #include "smc-envelope/SmartContractCode.h" #include "vm/dict.h" #include "td/utils/format.h" #include "td/utils/overloaded.h" #include "td/utils/Parser.h" #include "td/utils/Random.h" #include "block/block-auto.h" #include "block/block-parse.h" #include "common/util.h" namespace ton { td::StringBuilder& operator<<(td::StringBuilder& sb, const ManualDns::EntryData& data) { switch (data.type) { case ManualDns::EntryData::Type::Empty: return sb << "DELETED"; case ManualDns::EntryData::Type::Text: return sb << "TEXT:" << data.data.get().text; case ManualDns::EntryData::Type::NextResolver: return sb << "NEXT:" << data.data.get().resolver.rserialize(); case ManualDns::EntryData::Type::AdnlAddress: return sb << "ADNL:" << td::adnl_id_encode(data.data.get().adnl_address.as_slice()) .move_as_ok(); case ManualDns::EntryData::Type::SmcAddress: return sb << "SMC:" << data.data.get().smc_address.rserialize(); case ManualDns::EntryData::Type::StorageAddress: return sb << "STORAGE:" << data.data.get().bag_id.to_hex(); } return sb << ""; } //proto_list_nil$0 = ProtoList; //proto_list_next$1 head:Protocol tail:ProtoList = ProtoList; //proto_http#4854 = Protocol; //cap_list_nil$0 = SmcCapList; //cap_list_next$1 head:SmcCapability tail:SmcCapList = SmcCapList; //cap_method_seqno#5371 = SmcCapability; //cap_method_pubkey#71f4 = SmcCapability; //cap_is_wallet#2177 = SmcCapability; //cap_name#ff name:Text = SmcCapability; // td::Result> DnsInterface::EntryData::as_cell() const { td::Ref res; td::Status error; data.visit(td::overloaded( [&](const EntryDataText& text) { block::gen::DNSRecord::Record_dns_text dns; vm::CellBuilder cb; vm::CellText::store(cb, text.text); dns.x = vm::load_cell_slice_ref(cb.finalize()); tlb::pack_cell(res, dns); }, [&](const EntryDataNextResolver& resolver) { block::gen::DNSRecord::Record_dns_next_resolver dns; vm::CellBuilder cb; block::tlb::t_MsgAddressInt.store_std_address(cb, resolver.resolver.workchain, resolver.resolver.addr); dns.resolver = vm::load_cell_slice_ref(cb.finalize()); tlb::pack_cell(res, dns); }, [&](const EntryDataAdnlAddress& adnl_address) { block::gen::DNSRecord::Record_dns_adnl_address dns; dns.adnl_addr = adnl_address.adnl_address; dns.flags = 0; tlb::pack_cell(res, dns); }, [&](const EntryDataSmcAddress& smc_address) { block::gen::DNSRecord::Record_dns_smc_address dns; vm::CellBuilder cb; block::tlb::t_MsgAddressInt.store_std_address(cb, smc_address.smc_address.workchain, smc_address.smc_address.addr); dns.smc_addr = vm::load_cell_slice_ref(cb.finalize()); tlb::pack_cell(res, dns); }, [&](const EntryDataStorageAddress& storage_address) { block::gen::DNSRecord::Record_dns_storage_address dns; dns.bag_id = storage_address.bag_id; tlb::pack_cell(res, dns); })); if (error.is_error()) { return error; } if (res.is_null()) { return td::Status::Error("Entry data is emtpy"); } return res; //dns_text#1eda _:Text = DNSRecord; //dns_next_resolver#ba93 resolver:MsgAddressInt = DNSRecord; // usually in record #-1 //dns_adnl_address#ad01 adnl_addr:bits256 flags:(## 8) { flags <= 1 } proto_list:flags . 0?ProtoList = DNSRecord; // often in record #2 //dns_smc_address#9fd3 smc_addr:MsgAddressInt flags:(## 8) { flags <= 1 } cap_list:flags . 0?SmcCapList = DNSRecord; // often in record #1 } td::Result DnsInterface::EntryData::from_cellslice(vm::CellSlice& cs) { switch (block::gen::t_DNSRecord.get_tag(cs)) { case block::gen::DNSRecord::dns_text: { block::gen::DNSRecord::Record_dns_text dns; tlb::unpack(cs, dns); TRY_RESULT(text, vm::CellText::load(dns.x.write())); return EntryData::text(std::move(text)); } case block::gen::DNSRecord::dns_next_resolver: { block::gen::DNSRecord::Record_dns_next_resolver dns; tlb::unpack(cs, dns); ton::WorkchainId wc; ton::StdSmcAddress addr; if (!block::tlb::t_MsgAddressInt.extract_std_address(dns.resolver, wc, addr)) { return td::Status::Error("Invalid address"); } return EntryData::next_resolver(block::StdAddress(wc, addr)); } case block::gen::DNSRecord::dns_adnl_address: { block::gen::DNSRecord::Record_dns_adnl_address dns; tlb::unpack(cs, dns); return EntryData::adnl_address(dns.adnl_addr); } case block::gen::DNSRecord::dns_smc_address: { block::gen::DNSRecord::Record_dns_smc_address dns; tlb::unpack(cs, dns); ton::WorkchainId wc; ton::StdSmcAddress addr; if (!block::tlb::t_MsgAddressInt.extract_std_address(dns.smc_addr, wc, addr)) { return td::Status::Error("Invalid address"); } return EntryData::smc_address(block::StdAddress(wc, addr)); } case block::gen::DNSRecord::dns_storage_address: { block::gen::DNSRecord::Record_dns_storage_address dns; tlb::unpack(cs, dns); return EntryData::storage_address(dns.bag_id); } } return td::Status::Error("Unknown entry data"); } SmartContract::Args DnsInterface::resolve_args_raw(td::Slice encoded_name, td::Bits256 category, block::StdAddress address) { SmartContract::Args res; res.set_method_id("dnsresolve"); res.set_stack( {vm::load_cell_slice_ref(vm::CellBuilder().store_bytes(encoded_name).finalize()), td::bits_to_refint(category.cbits(), 256, false)}); res.set_address(std::move(address)); return res; } td::Result DnsInterface::resolve_args(td::Slice name, td::Bits256 category, block::StdAddress address) { if (name.size() > get_default_max_name_size()) { return td::Status::Error("Name is too long"); } auto encoded_name = encode_name(name); return resolve_args_raw(encoded_name, category, std::move(address)); } td::Result> DnsInterface::resolve(td::Slice name, td::Bits256 category) const { TRY_RESULT(raw_entries, resolve_raw(name, category)); std::vector entries; entries.reserve(raw_entries.size()); for (auto& raw_entry : raw_entries) { Entry entry; entry.name = std::move(raw_entry.name); entry.category = raw_entry.category; entry.partially_resolved = raw_entry.partially_resolved; auto cs = *raw_entry.data; auto data = EntryData::from_cellslice(cs); if (data.is_error()) { LOG(INFO) << "Failed to parse DNS entry: " << data.move_as_error(); } else { entry.data = data.move_as_ok(); entries.push_back(std::move(entry)); } } return entries; } /* 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:data] Operations (continuation of message): 00 Contract initialization message (only if seqno = 0) (x=-) 31 TSet: replace ENTIRE DOMAIN TABLE with the provided tree root cell (x=-) [Cell<1r>:new_domains_table] 51 OSet: replace owner public key with a new one (x=-) [UInt<256b>:new_public_key] */ // creation td::Ref ManualDns::create(td::Ref data, int revision) { return td::Ref( true, State{ton::SmartContractCode::get_code(ton::SmartContractCode::ManualDns, revision), std::move(data)}); } td::Ref ManualDns::create(const td::Ed25519::PublicKey& public_key, td::uint32 wallet_id, int revision) { return create(create_init_data_fast(public_key, wallet_id), revision); } td::optional ManualDns::guess_revision(const vm::Cell::Hash& code_hash) { for (auto i : ton::SmartContractCode::get_revisions(ton::SmartContractCode::ManualDns)) { if (ton::SmartContractCode::get_code(ton::SmartContractCode::ManualDns, i)->get_hash() == code_hash) { return i; } } return {}; } td::optional ManualDns::guess_revision(const block::StdAddress& address, const td::Ed25519::PublicKey& public_key, td::uint32 wallet_id) { for (auto i : {-1, 1}) { auto dns = ton::ManualDns::create(public_key, wallet_id, i); if (dns->get_address() == address) { return i; } } return {}; } td::Result ManualDns::get_wallet_id() const { return TRY_VM(get_wallet_id_or_throw()); } td::Result ManualDns::get_wallet_id_or_throw() const { if (state_.data.is_null()) { return 0; } //FIXME use get method return static_cast(vm::load_cell_slice(state_.data).fetch_ulong(32)); } td::Result> ManualDns::create_set_value_unsigned(td::Bits256 category, td::Slice name, td::Ref data) const { //11 VSet: set specified value to specified subdomain->category (x=2) //[Int<256b>:category] [Name:subdomain] [Cell<1r>:value] vm::CellBuilder cb; cb.store_long(11, 6); if (name.size() <= 58 - 32) { cb.store_bytes(category.as_slice()); cb.store_long(0, 1); cb.store_long(name.size(), 6); cb.store_bytes(name); } else { cb.store_bytes(category.as_slice()); cb.store_long(1, 1); cb.store_ref(vm::CellBuilder().store_bytes(name).finalize()); } cb.store_maybe_ref(std::move(data)); return cb.finalize(); } td::Result> ManualDns::create_delete_value_unsigned(td::Bits256 category, td::Slice name) const { //12 VDel: delete specified subdomain->category (x=2) //[Int<256b>:category] [Name:subdomain] vm::CellBuilder cb; cb.store_long(12, 6); if (name.size() <= 58 - 32) { cb.store_bytes(category.as_slice()); cb.store_long(0, 1); cb.store_long(name.size(), 6); cb.store_bytes(name); } else { cb.store_bytes(category.as_slice()); cb.store_long(1, 1); cb.store_ref(vm::CellBuilder().store_bytes(name).finalize()); } return cb.finalize(); } td::Result> ManualDns::create_delete_all_unsigned() const { // 32 TDel: nullify ENTIRE DOMAIN TABLE (x=-) vm::CellBuilder cb; cb.store_long(32, 6); return cb.finalize(); } td::Result> ManualDns::create_set_all_unsigned(td::Span entries) const { vm::PrefixDictionary pdict(1023); for (auto& action : entries) { auto name_key = encode_name(action.name); int zero_cnt = 0; for (auto c : name_key) { if (c == 0) { zero_cnt++; } } auto new_name_key = vm::load_cell_slice(vm::CellBuilder().store_long(zero_cnt, 7).store_bytes(name_key).finalize()); auto ptr = new_name_key.data_bits(); auto ptr_size = new_name_key.size(); auto o_dict = pdict.lookup(ptr, ptr_size); td::Ref dict_root; if (o_dict.not_null()) { o_dict->prefetch_maybe_ref(dict_root); } vm::Dictionary dict(dict_root, 256); if (!action.data.value().is_null()) { dict.set_ref(action.category.bits(), 256, action.data.value()); } pdict.set(ptr, ptr_size, dict.get_root()); } vm::CellBuilder cb; cb.store_long(31, 6); cb.store_maybe_ref(pdict.get_root_cell()); return cb.finalize(); } //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] td::Result> ManualDns::create_delete_name_unsigned(td::Slice name) const { vm::CellBuilder cb; cb.store_long(22, 6); if (name.size() <= 58) { cb.store_long(0, 1); cb.store_long(name.size(), 6); cb.store_bytes(name); } else { cb.store_long(1, 1); cb.store_ref(vm::CellBuilder().store_bytes(name).finalize()); } return cb.finalize(); } td::Result> ManualDns::create_set_name_unsigned(td::Slice name, td::Span entries) const { vm::CellBuilder cb; cb.store_long(21, 6); if (name.size() <= 58) { cb.store_long(0, 1); cb.store_long(name.size(), 6); cb.store_bytes(name); } else { cb.store_long(1, 1); cb.store_ref(vm::CellBuilder().store_bytes(name).finalize()); } vm::Dictionary dict(256); for (auto& action : entries) { if (action.data.value().is_null()) { continue; } dict.set_ref(action.category.cbits(), 256, action.data.value()); } cb.store_maybe_ref(dict.get_root_cell()); return cb.finalize(); } td::Result> ManualDns::prepare(td::Ref data, td::uint32 valid_until) const { TRY_RESULT(wallet_id, get_wallet_id()); auto hash = data->get_hash().as_slice().substr(28, 4).str(); vm::CellBuilder cb; cb.store_long(wallet_id, 32).store_long(valid_until, 32); //cb.store_bytes(hash); cb.store_long(td::Random::secure_uint32(), 32); cb.append_cellslice(vm::load_cell_slice(data)); return cb.finalize(); } td::Result> ManualDns::sign(const td::Ed25519::PrivateKey& private_key, td::Ref data) { auto signature = private_key.sign(data->get_hash().as_slice()).move_as_ok(); vm::CellBuilder cb; cb.store_bytes(signature.as_slice()); cb.append_cellslice(vm::load_cell_slice(data)); return cb.finalize(); } td::Result> ManualDns::create_init_query(const td::Ed25519::PrivateKey& private_key, td::uint32 valid_until) const { vm::CellBuilder cb; cb.store_long(0, 6); TRY_RESULT(prepared, prepare(cb.finalize(), valid_until)); return sign(private_key, std::move(prepared)); } td::Ref ManualDns::create_init_data_fast(const td::Ed25519::PublicKey& public_key, td::uint32 wallet_id) { vm::CellBuilder cb; cb.store_long(wallet_id, 32).store_long(0, 64).store_bytes(public_key.as_octet_string()); CHECK(cb.store_maybe_ref({})); CHECK(cb.store_maybe_ref({})); return cb.finalize(); } size_t ManualDns::get_max_name_size() const { return get_default_max_name_size(); } td::Result> ManualDns::resolve_raw(td::Slice name, td::Bits256 category) const { return TRY_VM(resolve_raw_or_throw(name, category)); } td::Result> ManualDns::resolve_raw_or_throw(td::Slice name, td::Bits256 category) const { if (name.size() > get_max_name_size()) { return td::Status::Error("Name is too long"); } auto encoded_name = encode_name(name); auto res = run_get_method(resolve_args_raw(encoded_name, category, address_)); if (!res.success) { return td::Status::Error("get method failed"); } std::vector vec; auto data = res.stack.write().pop_maybe_cell(); if (data.is_null()) { return vec; } size_t prefix_size = res.stack.write().pop_smallint_range((int)encoded_name.size() * 8); if (prefix_size % 8 != 0) { return td::Status::Error("Prefix size is not divisible by 8"); } prefix_size /= 8; if (prefix_size == 0) { return vec; } if (prefix_size < encoded_name.size()) { vec.push_back({decode_name(td::Slice(encoded_name).substr(0, prefix_size)), DNS_NEXT_RESOLVER_CATEGORY, vm::load_cell_slice_ref(data), true}); } else { if (category.is_zero()) { vm::Dictionary dict(std::move(data), 256); dict.check_for_each([&](td::Ref cs, td::ConstBitPtr key, int n) { CHECK(n == 256); if (cs.is_null() || cs->size_ext() != 0x10000) { return true; } cs = vm::load_cell_slice_ref(cs->prefetch_ref()); vec.push_back({name.str(), td::Bits256(key), cs}); return true; }); } else { vec.push_back({name.str(), category, vm::load_cell_slice_ref(data)}); } } return vec; } td::Result> ManualDns::create_update_query(CombinedActions& combined) const { if (combined.name.empty()) { if (combined.actions.value().empty()) { return create_delete_all_unsigned(); } return create_set_all_unsigned(combined.actions.value()); } if (combined.category.is_zero()) { if (!combined.actions) { return create_delete_name_unsigned(encode_name(combined.name)); } return create_set_name_unsigned(encode_name(combined.name), combined.actions.value()); } CHECK(combined.actions.value().size() == 1); auto& action = combined.actions.value()[0]; if (action.data) { return create_set_value_unsigned(action.category, encode_name(action.name), action.data.value()); } else { return create_delete_value_unsigned(action.category, encode_name(action.name)); } } td::Result> ManualDns::create_update_query(td::Ed25519::PrivateKey& pk, td::Span actions, td::uint32 valid_until) const { auto combined = combine_actions(actions); std::vector> queries; for (auto& c : combined) { TRY_RESULT(q, create_update_query(c)); queries.push_back(std::move(q)); } td::Ref combined_query; for (auto& query : td::reversed(queries)) { if (combined_query.is_null()) { combined_query = std::move(query); } else { auto next = vm::load_cell_slice(combined_query); combined_query = vm::CellBuilder() .append_cellslice(vm::load_cell_slice(query)) .store_ref(vm::CellBuilder().append_cellslice(next).finalize()) .finalize(); } } TRY_RESULT(prepared, prepare(std::move(combined_query), valid_until)); return sign(pk, std::move(prepared)); } std::string DnsInterface::encode_name(td::Slice name) { std::string res; if (name.empty() || name == ".") { res += '\0'; return res; } while (!name.empty()) { auto pos = name.rfind('.'); if (pos == td::Slice::npos) { res += name.str(); name = td::Slice(); } else { res += name.substr(pos + 1).str(); name.truncate(pos); } res += '\0'; } return res; } std::string DnsInterface::decode_name(td::Slice name) { std::string res; while (!name.empty()) { auto pos = name.rfind('\0'); if (pos == td::Slice::npos) { res += name.str(); name = td::Slice(); } else { res += name.substr(pos + 1).str(); name.truncate(pos); res += '.'; } } return res; } std::string ManualDns::serialize_data(const EntryData& data) { std::string res; data.data.visit( td::overloaded([&](const ton::ManualDns::EntryDataText& text) { res = "UNSUPPORTED"; }, [&](const ton::ManualDns::EntryDataNextResolver& resolver) { res = "UNSUPPORTED"; }, [&](const ton::ManualDns::EntryDataAdnlAddress& adnl_address) { res = "UNSUPPORTED"; }, [&](const ton::ManualDns::EntryDataSmcAddress& text) { res = "UNSUPPORTED"; }, [&](const ton::ManualDns::EntryDataStorageAddress& storage_address) { res = "UNSUPPORTED"; })); return res; } td::Result> ManualDns::parse_data(td::Slice cmd) { td::ConstParser parser(cmd); parser.skip_whitespaces(); auto type = parser.read_till(':'); parser.skip(':'); if (type == "TEXT") { return ManualDns::EntryData::text(parser.read_all().str()); } else if (type == "ADNL") { TRY_RESULT(address, td::adnl_id_decode(parser.read_all())); return ManualDns::EntryData::adnl_address(address); } else if (type == "SMC") { TRY_RESULT(address, block::StdAddress::parse(parser.read_all())); return ManualDns::EntryData::smc_address(address); } else if (type == "NEXT") { TRY_RESULT(address, block::StdAddress::parse(parser.read_all())); return ManualDns::EntryData::next_resolver(address); } else if (type == "STORAGE") { td::Bits256 bag_id; if (bag_id.from_hex(parser.read_all(), false) != 256) { return td::Status::Error("failed to parse bag id"); } return ManualDns::EntryData::storage_address(bag_id); } else if (parser.data() == "DELETED") { return {}; } return td::Status::Error(PSLICE() << "Unknown entry type: " << type); } td::Result ManualDns::parse_line(td::Slice cmd) { // Cmd = // set name category data | // delete.name name | // delete.all // data = // TEXT: | // SMC: | // NEXT: | // ADNL: // DELETED td::ConstParser parser(cmd); auto type = parser.read_word(); if (type == "set") { auto name = parser.read_word(); auto category_str = parser.read_word(); TRY_RESULT(data, parse_data(parser.read_all())); return ManualDns::ActionExt{name.str(), td::sha256_bits256(td::as_slice(category_str)), std::move(data)}; } else if (type == "delete.name") { auto name = parser.read_word(); if (name.empty()) { return td::Status::Error("name is empty"); } return ManualDns::ActionExt{name.str(), td::Bits256::zero(), {}}; } else if (type == "delete.all") { return ManualDns::ActionExt{"", td::Bits256::zero(), {}}; } return td::Status::Error(PSLICE() << "Unknown command: " << type); } td::Result> ManualDns::parse(td::Slice cmd) { auto lines = td::full_split(cmd, '\n'); std::vector res; res.reserve(lines.size()); for (auto& line : lines) { td::ConstParser parser(line); parser.skip_whitespaces(); if (parser.empty()) { continue; } TRY_RESULT(action, parse_line(parser.read_all())); res.push_back(std::move(action)); } return res; } } // namespace ton