// Copyright (c) 2014-2019, The Monero Project // Copyright (c) 2018, The Loki Project // // All rights reserved. // // Redistribution and use in source and binary forms, with or without modification, are // permitted provided that the following conditions are met: // // 1. Redistributions of source code must retain the above copyright notice, this list of // conditions and the following disclaimer. // // 2. Redistributions in binary form must reproduce the above copyright notice, this list // of conditions and the following disclaimer in the documentation and/or other // materials provided with the distribution. // // 3. Neither the name of the copyright holder nor the names of its contributors may be // used to endorse or promote products derived from this software without specific // prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL // THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, // STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // // Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers #include #include "epee/string_tools.h" #include #include #include #include extern "C" { #include #ifdef ENABLE_SYSTEMD # include #endif } #include #include "cryptonote_core.h" #include "uptime_proof.h" #include "common/file.h" #include "common/sha256sum.h" #include "common/threadpool.h" #include "common/command_line.h" #include "common/hex.h" #include "common/base58.h" #include "epee/warnings.h" #include "crypto/crypto.h" #include "cryptonote_config.h" #include "cryptonote_basic/hardfork.h" #include #include "checkpoints/checkpoints.h" #include "ringct/rctTypes.h" #include "blockchain_db/blockchain_db.h" #include "blockchain_db/sqlite/db_sqlite.h" #include "ringct/rctSigs.h" #include "common/notify.h" #include "version.h" #include "epee/memwipe.h" #include "common/i18n.h" #include "epee/net/local_ip.h" #include "logging/oxen_logger.h" #include #include #include DISABLE_VS_WARNINGS(4355) #define BAD_SEMANTICS_TXES_MAX_SIZE 100 // basically at least how many bytes the block itself serializes to without the miner tx #define BLOCK_SIZE_SANITY_LEEWAY 100 namespace cryptonote { static auto logcat = oxen::log::Cat("cn"); static auto omqlogcat = oxen::log::Cat("omq"); const command_line::arg_descriptor arg_testnet_on = { "testnet" , "Run on testnet. The wallet must be launched with --testnet flag." , false }; const command_line::arg_descriptor arg_devnet_on = { "devnet" , "Run on devnet. The wallet must be launched with --devnet flag." , false }; const command_line::arg_descriptor arg_regtest_on = { "regtest" , "Run in a regression testing mode." , false }; const command_line::arg_descriptor arg_keep_fakechain = { "keep-fakechain" , "Don't delete any existing database when in fakechain mode." , false }; const command_line::arg_descriptor arg_fixed_difficulty = { "fixed-difficulty" , "Fixed difficulty used for testing." , 0 }; const command_line::arg_descriptor arg_dev_allow_local = { "dev-allow-local-ips" , "Allow a local IPs for local and received service node public IP (for local testing only)" , false }; const command_line::arg_descriptor arg_data_dir = { "data-dir" , "Specify data directory" , tools::get_default_data_dir().u8string() , {{ &arg_testnet_on, &arg_devnet_on }} , [](std::array testnet_devnet, bool defaulted, std::string val)->std::string { if (testnet_devnet[0]) return (fs::u8path(val) / "testnet").u8string(); else if (testnet_devnet[1]) return (fs::u8path(val) / "devnet").u8string(); return val; } }; const command_line::arg_descriptor arg_offline = { "offline" , "Do not listen for peers, nor connect to any" }; const command_line::arg_descriptor arg_block_download_max_size = { "block-download-max-size" , "Set maximum size of block download queue in bytes (0 for default)" , 0 }; static const command_line::arg_descriptor arg_test_drop_download = { "test-drop-download" , "For net tests: in download, discard ALL blocks instead checking/saving them (very fast)" }; static const command_line::arg_descriptor arg_test_drop_download_height = { "test-drop-download-height" , "Like test-drop-download but discards only after around certain height" , 0 }; static const command_line::arg_descriptor arg_fast_block_sync = { "fast-block-sync" , "Sync up most of the way by using embedded, known block hashes." , 1 }; static const command_line::arg_descriptor arg_prep_blocks_threads = { "prep-blocks-threads" , "Max number of threads to use when preparing block hashes in groups." , 4 }; static const command_line::arg_descriptor arg_show_time_stats = { "show-time-stats" , "Show time-stats when processing blocks/txs and disk synchronization." , 0 }; static const command_line::arg_descriptor arg_block_sync_size = { "block-sync-size" , "How many blocks to sync at once during chain synchronization (0 = adaptive)." , 0 }; static const command_line::arg_descriptor arg_pad_transactions = { "pad-transactions" , "Pad relayed transactions to help defend against traffic volume analysis" , false }; static const command_line::arg_descriptor arg_max_txpool_weight = { "max-txpool-weight" , "Set maximum txpool weight in bytes." , DEFAULT_MEMPOOL_MAX_WEIGHT }; static const command_line::arg_descriptor arg_service_node = { "service-node" , "Run as a service node, option 'service-node-public-ip' must be set" }; static const command_line::arg_descriptor arg_public_ip = { "service-node-public-ip" , "Public IP address on which this service node's services (such as the Loki " "storage server) are accessible. This IP address will be advertised to the " "network via the service node uptime proofs. Required if operating as a " "service node." }; static const command_line::arg_descriptor arg_storage_server_port = { "storage-server-port", "Deprecated option, ignored.", 0}; static const command_line::arg_descriptor arg_quorumnet_port = { "quorumnet-port" , "The port on which this service node listen for direct connections from other " "service nodes for quorum messages. The port must be publicly reachable " "on the `--service-node-public-ip' address and binds to the p2p IP address." " Only applies when running as a service node." , config::QNET_DEFAULT_PORT , {{ &cryptonote::arg_testnet_on, &cryptonote::arg_devnet_on }} , [](std::array testnet_devnet, bool defaulted, uint16_t val) -> uint16_t { return defaulted && testnet_devnet[0] ? config::testnet::QNET_DEFAULT_PORT : defaulted && testnet_devnet[1] ? config::devnet::QNET_DEFAULT_PORT : val; } }; static const command_line::arg_descriptor arg_omq_quorumnet_public{ "lmq-public-quorumnet", "Allow the curve-enabled quorumnet address (for a Service Node) to be used for public RPC commands as if passed to --lmq-curve-public. " "Note that even without this option the quorumnet port can be used for RPC commands by --lmq-admin and --lmq-user pubkeys.", false}; static const command_line::arg_descriptor arg_block_notify = { "block-notify" , "Run a program for each new block, '%s' will be replaced by the block hash" , "" }; static const command_line::arg_descriptor arg_prune_blockchain = { "prune-blockchain" , "Prune blockchain" , false }; static const command_line::arg_descriptor arg_reorg_notify = { "reorg-notify" , "Run a program for each reorg, '%s' will be replaced by the split height, " "'%h' will be replaced by the new blockchain height, and '%n' will be " "replaced by the number of new blocks in the new chain" , "" }; static const command_line::arg_descriptor arg_keep_alt_blocks = { "keep-alt-blocks" , "Keep alternative blocks on restart" , false }; static const command_line::arg_descriptor arg_store_quorum_history = { "store-quorum-history", "Store the service node quorum history for the last N blocks to allow historic quorum lookups " "(e.g. by a block explorer). Specify the number of blocks of history to store, or 1 to store " "the entire history. Requires considerably more memory and block chain storage.", 0}; // Loads stubs that fail if invoked. The stubs are replaced in the cryptonote_protocol/quorumnet.cpp glue code. [[noreturn]] static void need_core_init(std::string_view stub_name) { throw std::logic_error("Internal error: core callback initialization was not performed for "s + std::string(stub_name)); } void (*long_poll_trigger)(tx_memory_pool& pool) = [](tx_memory_pool&) { need_core_init("long_poll_trigger"sv); }; quorumnet_new_proc *quorumnet_new = [](core&) -> void* { need_core_init("quorumnet_new"sv); }; quorumnet_init_proc *quorumnet_init = [](core&, void*) { need_core_init("quorumnet_init"sv); }; quorumnet_delete_proc *quorumnet_delete = [](void*&) { need_core_init("quorumnet_delete"sv); }; quorumnet_relay_obligation_votes_proc *quorumnet_relay_obligation_votes = [](void*, const std::vector&) { need_core_init("quorumnet_relay_obligation_votes"sv); }; quorumnet_send_blink_proc *quorumnet_send_blink = [](core&, const std::string&) -> std::future> { need_core_init("quorumnet_send_blink"sv); }; quorumnet_pulse_relay_message_to_quorum_proc *quorumnet_pulse_relay_message_to_quorum = [](void *, pulse::message const &, service_nodes::quorum const &, bool) -> void { need_core_init("quorumnet_pulse_relay_message_to_quorum"sv); }; //----------------------------------------------------------------------------------------------- core::core() : m_mempool(m_blockchain_storage) , m_service_node_list(m_blockchain_storage) , m_blockchain_storage(m_mempool, m_service_node_list) , m_quorum_cop(*this) , m_miner(this, [this](const cryptonote::block &b, uint64_t height, unsigned int threads, crypto::hash &hash) { hash = cryptonote::get_block_longhash_w_blockchain(m_nettype, &m_blockchain_storage, b, height, threads); return true; }) , m_pprotocol(&m_protocol_stub) , m_starter_message_showed(false) , m_target_blockchain_height(0) , m_last_json_checkpoints_update(0) , m_nettype(network_type::UNDEFINED) , m_last_storage_server_ping(0) , m_last_lokinet_ping(0) , m_pad_transactions(false) , ss_version{0} , lokinet_version{0} { m_checkpoints_updating.clear(); } void core::set_cryptonote_protocol(i_cryptonote_protocol* pprotocol) { if(pprotocol) m_pprotocol = pprotocol; else m_pprotocol = &m_protocol_stub; } //----------------------------------------------------------------------------------------------- bool core::update_checkpoints_from_json_file() { if (m_checkpoints_updating.test_and_set()) return true; // load json checkpoints every 10min and verify them with respect to what blocks we already have bool res = true; if (time(NULL) - m_last_json_checkpoints_update >= 600) { res = m_blockchain_storage.update_checkpoints_from_json_file(m_checkpoints_path); m_last_json_checkpoints_update = time(NULL); } m_checkpoints_updating.clear(); // if anything fishy happened getting new checkpoints, bring down the house if (!res) { graceful_exit(); } return res; } //----------------------------------------------------------------------------------- void core::stop() { m_miner.stop(); m_blockchain_storage.cancel(); } //----------------------------------------------------------------------------------- void core::init_options(boost::program_options::options_description& desc) { command_line::add_arg(desc, arg_data_dir); command_line::add_arg(desc, arg_test_drop_download); command_line::add_arg(desc, arg_test_drop_download_height); command_line::add_arg(desc, arg_testnet_on); command_line::add_arg(desc, arg_devnet_on); command_line::add_arg(desc, arg_regtest_on); command_line::add_arg(desc, arg_keep_fakechain); command_line::add_arg(desc, arg_fixed_difficulty); command_line::add_arg(desc, arg_dev_allow_local); command_line::add_arg(desc, arg_prep_blocks_threads); command_line::add_arg(desc, arg_fast_block_sync); command_line::add_arg(desc, arg_show_time_stats); command_line::add_arg(desc, arg_block_sync_size); command_line::add_arg(desc, arg_offline); command_line::add_arg(desc, arg_block_download_max_size); command_line::add_arg(desc, arg_max_txpool_weight); command_line::add_arg(desc, arg_service_node); command_line::add_arg(desc, arg_public_ip); command_line::add_arg(desc, arg_storage_server_port); command_line::add_arg(desc, arg_quorumnet_port); command_line::add_arg(desc, arg_pad_transactions); command_line::add_arg(desc, arg_block_notify); #if 0 // TODO(oxen): Pruning not supported because of Service Node List command_line::add_arg(desc, arg_prune_blockchain); #endif command_line::add_arg(desc, arg_reorg_notify); command_line::add_arg(desc, arg_keep_alt_blocks); command_line::add_arg(desc, arg_store_quorum_history); command_line::add_arg(desc, arg_omq_quorumnet_public); miner::init_options(desc); BlockchainDB::init_options(desc); } //----------------------------------------------------------------------------------------------- bool core::handle_command_line(const boost::program_options::variables_map& vm) { if (m_nettype != network_type::FAKECHAIN) { const bool testnet = command_line::get_arg(vm, arg_testnet_on); const bool devnet = command_line::get_arg(vm, arg_devnet_on); m_nettype = testnet ? network_type::TESTNET : devnet ? network_type::DEVNET : network_type::MAINNET; } m_check_uptime_proof_interval.interval(get_net_config().UPTIME_PROOF_CHECK_INTERVAL); m_config_folder = fs::u8path(command_line::get_arg(vm, arg_data_dir)); test_drop_download_height(command_line::get_arg(vm, arg_test_drop_download_height)); m_pad_transactions = get_arg(vm, arg_pad_transactions); m_offline = get_arg(vm, arg_offline); if (command_line::get_arg(vm, arg_test_drop_download) == true) test_drop_download(); if (command_line::get_arg(vm, arg_dev_allow_local)) m_service_node_list.debug_allow_local_ips = true; m_service_node = command_line::get_arg(vm, arg_service_node); if (m_service_node) { /// TODO: parse these options early, before we start p2p server etc? m_quorumnet_port = command_line::get_arg(vm, arg_quorumnet_port); bool args_okay = true; if (m_quorumnet_port == 0) { oxen::log::error(logcat, "Quorumnet port cannot be 0; please specify a valid port to listen on with: '--{} '", arg_quorumnet_port.name); args_okay = false; } const std::string pub_ip = command_line::get_arg(vm, arg_public_ip); if (pub_ip.size()) { if (!epee::string_tools::get_ip_int32_from_string(m_sn_public_ip, pub_ip)) { oxen::log::error(logcat, "Unable to parse IPv4 public address from: {}", pub_ip); args_okay = false; } if (!epee::net_utils::is_ip_public(m_sn_public_ip)) { if (m_service_node_list.debug_allow_local_ips) { oxen::log::warning(logcat, "Address given for public-ip is not public; allowing it because dev-allow-local-ips was specified. This service node WILL NOT WORK ON THE PUBLIC OXEN NETWORK!"); } else { oxen::log::error(logcat, "Address given for public-ip is not public: {}", epee::string_tools::get_ip_string_from_int32(m_sn_public_ip)); args_okay = false; } } } else { oxen::log::error(logcat, "Please specify an IPv4 public address which the service node & storage server is accessible from with: '--{} '", arg_public_ip.name); args_okay = false; } if (!args_okay) { oxen::log::error(logcat, "IMPORTANT: One or more required service node-related configuration settings/options were omitted or invalid please fix them and restart oxend."); return false; } } return true; } //----------------------------------------------------------------------------------------------- uint64_t core::get_current_blockchain_height() const { return m_blockchain_storage.get_current_blockchain_height(); } //----------------------------------------------------------------------------------------------- std::pair core::get_blockchain_top() const { std::pair result; result.second = m_blockchain_storage.get_tail_id(result.first); return result; } //----------------------------------------------------------------------------------------------- bool core::get_blocks(uint64_t start_offset, size_t count, std::vector>& blocks, std::vector& txs) const { return m_blockchain_storage.get_blocks(start_offset, count, blocks, txs); } //----------------------------------------------------------------------------------------------- bool core::get_blocks(uint64_t start_offset, size_t count, std::vector>& blocks) const { return m_blockchain_storage.get_blocks(start_offset, count, blocks); } //----------------------------------------------------------------------------------------------- bool core::get_blocks(uint64_t start_offset, size_t count, std::vector& blocks) const { return m_blockchain_storage.get_blocks_only(start_offset, count, blocks); } //----------------------------------------------------------------------------------------------- bool core::get_blocks(const std::vector& block_ids, std::vector> blocks, std::unordered_set* missed_bs) const { return m_blockchain_storage.get_blocks(block_ids, blocks, missed_bs); } //----------------------------------------------------------------------------------------------- bool core::get_transactions(const std::vector& txs_ids, std::vector& txs, std::unordered_set* missed_txs) const { return m_blockchain_storage.get_transactions_blobs(txs_ids, txs, missed_txs); } //----------------------------------------------------------------------------------------------- bool core::get_split_transactions_blobs(const std::vector& txs_ids, std::vector>& txs, std::unordered_set* missed_txs) const { return m_blockchain_storage.get_split_transactions_blobs(txs_ids, txs, missed_txs); } //----------------------------------------------------------------------------------------------- bool core::get_transactions(const std::vector& txs_ids, std::vector& txs, std::unordered_set* missed_txs) const { return m_blockchain_storage.get_transactions(txs_ids, txs, missed_txs); } //----------------------------------------------------------------------------------------------- bool core::get_alternative_blocks(std::vector& blocks) const { return m_blockchain_storage.get_alternative_blocks(blocks); } //----------------------------------------------------------------------------------------------- size_t core::get_alternative_blocks_count() const { return m_blockchain_storage.get_alternative_blocks_count(); } static std::string time_ago_str(time_t now, time_t then) { if (then >= now) return "now"s; if (then == 0) return "never"s; int seconds = now - then; if (seconds >= 60) return std::to_string(seconds / 60) + "m" + std::to_string(seconds % 60) + "s"; return std::to_string(seconds % 60) + "s"; } // Returns a bool on whether the service node is currently active bool core::is_active_sn() const { auto info = get_my_sn_info(); return (info && info->is_active()); } // Returns the service nodes info std::shared_ptr core::get_my_sn_info() const { auto& snl = get_service_node_list(); const auto& pubkey = get_service_keys().pub; auto states = snl.get_service_node_list_state({ pubkey }); if (states.empty()) return nullptr; else { return states[0].info; } } // Returns a string for systemd status notifications such as: // Height: 1234567, SN: active, proof: 55m12s, storage: 4m48s, lokinet: 47s std::string core::get_status_string() const { std::string s; s.reserve(128); s += 'v'; s += OXEN_VERSION_STR; s += "; Height: "; s += std::to_string(get_blockchain_storage().get_current_blockchain_height()); s += ", SN: "; if (!service_node()) s += "no"; else { auto& snl = get_service_node_list(); const auto& pubkey = get_service_keys().pub; auto states = snl.get_service_node_list_state({ pubkey }); if (states.empty()) s += "not registered"; else { auto &info = *states[0].info; if (!info.is_fully_funded()) s += "awaiting contr."; else if (info.is_active()) s += "active"; else if (info.is_decommissioned()) s += "decomm."; uint64_t last_proof = 0; snl.access_proof(pubkey, [&](auto& proof) { last_proof = proof.timestamp; }); s += ", proof: "; time_t now = std::time(nullptr); s += time_ago_str(now, last_proof); s += ", storage: "; s += time_ago_str(now, m_last_storage_server_ping); s += ", lokinet: "; s += time_ago_str(now, m_last_lokinet_ping); } } return s; } //----------------------------------------------------------------------------------------------- bool core::init(const boost::program_options::variables_map& vm, const cryptonote::test_options *test_options, const GetCheckpointsCallback& get_checkpoints/* = nullptr */) { start_time = std::time(nullptr); const bool regtest = command_line::get_arg(vm, arg_regtest_on); if (test_options != NULL || regtest) { m_nettype = network_type::FAKECHAIN; } bool r = handle_command_line(vm); /// Currently terminating before blockchain is initialized results in a crash /// during deinitialization... TODO: fix that CHECK_AND_ASSERT_MES(r, false, "Failed to apply command line options."); std::string db_sync_mode = command_line::get_arg(vm, cryptonote::arg_db_sync_mode); bool db_salvage = command_line::get_arg(vm, cryptonote::arg_db_salvage) != 0; bool fast_sync = command_line::get_arg(vm, arg_fast_block_sync) != 0; uint64_t blocks_threads = command_line::get_arg(vm, arg_prep_blocks_threads); size_t max_txpool_weight = command_line::get_arg(vm, arg_max_txpool_weight); bool const prune_blockchain = false; /* command_line::get_arg(vm, arg_prune_blockchain); */ bool keep_alt_blocks = command_line::get_arg(vm, arg_keep_alt_blocks); bool keep_fakechain = command_line::get_arg(vm, arg_keep_fakechain); r = init_service_keys(); CHECK_AND_ASSERT_MES(r, false, "Failed to create or load service keys"); if (m_service_node) { // Only use our service keys for our service node if we are running in SN mode: m_service_node_list.set_my_service_node_keys(&m_service_keys); } auto folder = m_config_folder; if (m_nettype == network_type::FAKECHAIN) folder /= "fake"; // make sure the data directory exists, and try to lock it if (std::error_code ec; !fs::is_directory(folder, ec) && !fs::create_directories(folder, ec) && ec) { oxen::log::error(logcat, "Failed to create directory " + folder.u8string() + (ec ? ": " + ec.message() : ""s)); return false; } std::unique_ptr db(new_db()); if (!db) { oxen::log::error(logcat, "Failed to initialize a database"); return false; } auto ons_db_file_path = folder / "ons.db"; if(fs::exists(folder / "lns.db")) ons_db_file_path = folder / "lns.db"; auto sqlite_db_file_path = folder / "sqlite.db"; if (m_nettype == network_type::FAKECHAIN) { sqlite_db_file_path = ":memory:"; } auto sqliteDB = std::make_shared(m_nettype, sqlite_db_file_path); folder /= db->get_db_name(); oxen::log::info(logcat, "Loading blockchain from folder {} ...", folder); // default to fast:async:1 if overridden blockchain_db_sync_mode sync_mode = db_defaultsync; bool sync_on_blocks = true; uint64_t sync_threshold = 1; if (m_nettype == network_type::FAKECHAIN && !keep_fakechain) { // reset the db by removing the database file before opening it if (!db->remove_data_file(folder)) { oxen::log::error(logcat, "Failed to remove data file in {}", folder); return false; } fs::remove(ons_db_file_path); } try { uint64_t db_flags = 0; std::vector options; boost::trim(db_sync_mode); boost::split(options, db_sync_mode, boost::is_any_of(" :")); const bool db_sync_mode_is_default = command_line::is_arg_defaulted(vm, cryptonote::arg_db_sync_mode); for(const auto &option : options) oxen::log::debug(logcat, "option: {}", option); // default to fast:async:1 uint64_t DEFAULT_FLAGS = DBF_FAST; if(options.size() == 0) { // default to fast:async:1 db_flags = DEFAULT_FLAGS; } bool safemode = false; if(options.size() >= 1) { if(options[0] == "safe") { safemode = true; db_flags = DBF_SAFE; sync_mode = db_sync_mode_is_default ? db_defaultsync : db_nosync; } else if(options[0] == "fast") { db_flags = DBF_FAST; sync_mode = db_sync_mode_is_default ? db_defaultsync : db_async; } else if(options[0] == "fastest") { db_flags = DBF_FASTEST; sync_threshold = 1000; // default to fastest:async:1000 sync_mode = db_sync_mode_is_default ? db_defaultsync : db_async; } else db_flags = DEFAULT_FLAGS; } if(options.size() >= 2 && !safemode) { if(options[1] == "sync") sync_mode = db_sync_mode_is_default ? db_defaultsync : db_sync; else if(options[1] == "async") sync_mode = db_sync_mode_is_default ? db_defaultsync : db_async; } if(options.size() >= 3 && !safemode) { char *endptr; uint64_t threshold = strtoull(options[2].c_str(), &endptr, 0); if (*endptr == '\0' || !strcmp(endptr, "blocks")) { sync_on_blocks = true; sync_threshold = threshold; } else if (!strcmp(endptr, "bytes")) { sync_on_blocks = false; sync_threshold = threshold; } else { oxen::log::error(logcat, "Invalid db sync mode: {}", options[2]); return false; } } if (db_salvage) db_flags |= DBF_SALVAGE; db->open(folder, m_nettype, db_flags); if(!db->m_open) return false; } catch (const DB_ERROR& e) { oxen::log::error(logcat, "Error opening database: {}", e.what()); return false; } m_blockchain_storage.set_user_options(blocks_threads, sync_on_blocks, sync_threshold, sync_mode, fast_sync); // We need this hook to get added before the block hook below, so that it fires first and // catches the start of a reorg before the block hook fires for the block in the reorg. try { if (!command_line::is_arg_defaulted(vm, arg_reorg_notify)) m_blockchain_storage.hook_block_post_add( [this, notify=tools::Notify(command_line::get_arg(vm, arg_reorg_notify))] (const auto& info) { if (!info.reorg) return; auto h = get_current_blockchain_height(); notify.notify( "%s", info.split_height, "%h", h, "%n", h - info.split_height); }); } catch (const std::exception &e) { oxen::log::error(logcat, "Failed to parse reorg notify spec"); } try { if (!command_line::is_arg_defaulted(vm, arg_block_notify)) m_blockchain_storage.hook_block_post_add( [notify=tools::Notify(command_line::get_arg(vm, arg_block_notify))] (const auto& info) { notify.notify("%s", tools::type_to_hex(get_block_hash(info.block))); }); } catch (const std::exception &e) { oxen::log::error(logcat, "Failed to parse block rate notify spec"); } cryptonote::test_options regtest_test_options{}; for (auto [it, end] = get_hard_forks(network_type::MAINNET); it != end; it++) { regtest_test_options.hard_forks.push_back(hard_fork{ it->version, it->snode_revision, regtest_test_options.hard_forks.size(), std::time(nullptr)}); } // Service Nodes m_service_node_list.set_quorum_history_storage(command_line::get_arg(vm, arg_store_quorum_history)); // NOTE: Implicit dependency. Service node list needs to be hooked before checkpoints. m_blockchain_storage.hook_blockchain_detached([this] (const auto& info) { m_service_node_list.blockchain_detached(info.height); }); m_blockchain_storage.hook_init([this] { m_service_node_list.init(); }); m_blockchain_storage.hook_validate_miner_tx([this] (const auto& info) { m_service_node_list.validate_miner_tx(info); }); m_blockchain_storage.hook_alt_block_add([this] (const auto& info) { m_service_node_list.alt_block_add(info); }); // NOTE: There is an implicit dependency on service node lists being hooked first! m_blockchain_storage.hook_init([this] { m_quorum_cop.init(); }); m_blockchain_storage.hook_block_add([this] (const auto& info) { m_quorum_cop.block_add(info.block, info.txs); }); m_blockchain_storage.hook_blockchain_detached([this] (const auto& info) { m_quorum_cop.blockchain_detached(info.height, info.by_pop_blocks); }); m_blockchain_storage.hook_block_post_add([this] (const auto&) { update_omq_sns(); }); // Checkpoints m_checkpoints_path = m_config_folder / fs::u8path(JSON_HASH_FILE_NAME); sqlite3 *ons_db = ons::init_oxen_name_system(ons_db_file_path, db->is_read_only()); if (!ons_db) return false; init_oxenmq(vm); const difficulty_type fixed_difficulty = command_line::get_arg(vm, arg_fixed_difficulty); r = m_blockchain_storage.init(db.release(), ons_db, std::move(sqliteDB), m_nettype, m_offline, regtest ? ®test_test_options : test_options, fixed_difficulty, get_checkpoints); CHECK_AND_ASSERT_MES(r, false, "Failed to initialize blockchain storage"); r = m_mempool.init(max_txpool_weight); CHECK_AND_ASSERT_MES(r, false, "Failed to initialize memory pool"); // now that we have a valid m_blockchain_storage, we can clean out any // transactions in the pool that do not conform to the current fork m_mempool.validate(m_blockchain_storage.get_network_version()); bool show_time_stats = command_line::get_arg(vm, arg_show_time_stats) != 0; m_blockchain_storage.set_show_time_stats(show_time_stats); block_sync_size = command_line::get_arg(vm, arg_block_sync_size); if (block_sync_size > BLOCKS_SYNCHRONIZING_MAX_COUNT) oxen::log::error(logcat, "Error --block-sync-size cannot be greater than {}", BLOCKS_SYNCHRONIZING_MAX_COUNT); oxen::log::info(logcat, "Loading checkpoints"); CHECK_AND_ASSERT_MES(update_checkpoints_from_json_file(), false, "One or more checkpoints loaded from json conflicted with existing checkpoints."); r = m_miner.init(vm, m_nettype); CHECK_AND_ASSERT_MES(r, false, "Failed to initialize miner instance"); if (!keep_alt_blocks && !m_blockchain_storage.get_db().is_read_only()) m_blockchain_storage.get_db().drop_alt_blocks(); if (prune_blockchain) { // display a message if the blockchain is not pruned yet if (!m_blockchain_storage.get_blockchain_pruning_seed()) { oxen::log::info(logcat, "Pruning blockchain..."); CHECK_AND_ASSERT_MES(m_blockchain_storage.prune_blockchain(), false, "Failed to prune blockchain"); } else { CHECK_AND_ASSERT_MES(m_blockchain_storage.update_blockchain_pruning(), false, "Failed to update blockchain pruning"); } } return true; } /// Loads a key pair from disk, if it exists, otherwise generates a new key pair and saves it to /// disk. /// /// get_pubkey - a function taking (privkey &, pubkey &) that sets the pubkey from the privkey; /// returns true for success/false for failure /// generate_pair - a void function taking (privkey &, pubkey &) that sets them to the generated values; can throw on error. template bool init_key(const fs::path &keypath, Privkey &privkey, Pubkey &pubkey, GetPubkey get_pubkey, GeneratePair generate_pair) { std::error_code ec; if (fs::exists(keypath, ec)) { std::string keystr; bool r = tools::slurp_file(keypath, keystr); memcpy(&unwrap(unwrap(privkey)), keystr.data(), sizeof(privkey)); memwipe(&keystr[0], keystr.size()); CHECK_AND_ASSERT_MES(r, false, "failed to load service node key from " + keypath.u8string()); CHECK_AND_ASSERT_MES(keystr.size() == sizeof(privkey), false, "service node key file " + keypath.u8string() + " has an invalid size"); r = get_pubkey(privkey, pubkey); CHECK_AND_ASSERT_MES(r, false, "failed to generate pubkey from secret key"); } else { try { generate_pair(privkey, pubkey); } catch (const std::exception& e) { oxen::log::error(logcat, "failed to generate keypair {}", e.what()); return false; } bool r = tools::dump_file(keypath, tools::view_guts(privkey)); CHECK_AND_ASSERT_MES(r, false, "failed to save service node key to " + keypath.u8string()); fs::permissions(keypath, fs::perms::owner_read, ec); } return true; } //----------------------------------------------------------------------------------------------- bool core::init_service_keys() { auto& keys = m_service_keys; static_assert( sizeof(crypto::ed25519_public_key) == crypto_sign_ed25519_PUBLICKEYBYTES && sizeof(crypto::ed25519_secret_key) == crypto_sign_ed25519_SECRETKEYBYTES && sizeof(crypto::ed25519_signature) == crypto_sign_BYTES && sizeof(crypto::x25519_public_key) == crypto_scalarmult_curve25519_BYTES && sizeof(crypto::x25519_secret_key) == crypto_scalarmult_curve25519_BYTES, "Invalid ed25519/x25519 sizes"); // /key_ed25519: Standard ed25519 secret key. We always have this, and generate one if it // doesn't exist. // // As of Loki 8.x, if this exists and `key` doesn't, we use this key for everything. For // compatibility with earlier versions we also allow `key` to contain a separate monero privkey // for the SN keypair. (The main difference is that the Monero keypair is unclamped and that it // only contains the private key value but not the secret key value that we need for full // Ed25519 signing). // if (!init_key(m_config_folder / "key_ed25519", keys.key_ed25519, keys.pub_ed25519, [](crypto::ed25519_secret_key &sk, crypto::ed25519_public_key &pk) { crypto_sign_ed25519_sk_to_pk(pk.data, sk.data); return true; }, [](crypto::ed25519_secret_key &sk, crypto::ed25519_public_key &pk) { crypto_sign_ed25519_keypair(pk.data, sk.data); }) ) return false; // Standard x25519 keys generated from the ed25519 keypair, used for encrypted communication between SNs int rc = crypto_sign_ed25519_pk_to_curve25519(keys.pub_x25519.data, keys.pub_ed25519.data); CHECK_AND_ASSERT_MES(rc == 0, false, "failed to convert ed25519 pubkey to x25519"); crypto_sign_ed25519_sk_to_curve25519(keys.key_x25519.data, keys.key_ed25519.data); // Legacy primary SN key file; we only load this if it exists, otherwise we use `key_ed25519` // for the primary SN keypair. (This key predates the Ed25519 keys and so is needed for // backwards compatibility with existing active service nodes.) The legacy key consists of // *just* the private point, but not the seed, and so cannot be used for full Ed25519 signatures // (which rely on the seed for signing). if (m_service_node) { if (std::error_code ec; !fs::exists(m_config_folder / "key", ec)) { epee::wipeable_string privkey_signhash; privkey_signhash.resize(crypto_hash_sha512_BYTES); unsigned char* pk_sh_data = reinterpret_cast(privkey_signhash.data()); crypto_hash_sha512(pk_sh_data, keys.key_ed25519.data, 32 /* first 32 bytes are the seed to be SHA512 hashed (the last 32 are just the pubkey) */); // Clamp private key (as libsodium does and expects -- see https://www.jcraige.com/an-explainer-on-ed25519-clamping if you want the broader reasons) pk_sh_data[0] &= 248; pk_sh_data[31] &= 63; // (some implementations put 127 here, but with the |64 in the next line it is the same thing) pk_sh_data[31] |= 64; // Monero crypto requires a pointless check that the secret key is < basepoint, so calculate // it mod basepoint to make it happy: sc_reduce32(pk_sh_data); std::memcpy(keys.key.data, pk_sh_data, 32); if (!crypto::secret_key_to_public_key(keys.key, keys.pub)) throw std::runtime_error{"Failed to derive primary key from ed25519 key"}; assert(0 == std::memcmp(keys.pub.data, keys.pub_ed25519.data, 32)); } else if (!init_key(m_config_folder / "key", keys.key, keys.pub, crypto::secret_key_to_public_key, [](crypto::secret_key &key, crypto::public_key &pubkey) { throw std::runtime_error{"Internal error: old-style public keys are no longer generated"}; })) return false; } else { keys.key = crypto::null_skey; keys.pub = crypto::null_pkey; } if (m_service_node) { oxen::log::info(logcat, fmt::format(fg(fmt::terminal_color::yellow), "Service node public keys:")); oxen::log::info(logcat, fmt::format(fg(fmt::terminal_color::yellow), "- primary: {}", tools::type_to_hex(keys.pub))); oxen::log::info(logcat, fmt::format(fg(fmt::terminal_color::yellow), "- ed25519: {}", tools::type_to_hex(keys.pub_ed25519))); // .snode address is the ed25519 pubkey, encoded with base32z and with .snode appended: oxen::log::info(logcat, fmt::format(fg(fmt::terminal_color::yellow), "- lokinet: {}.snode", oxenc::to_base32z(tools::view_guts(keys.pub_ed25519)))); oxen::log::info(logcat, fmt::format(fg(fmt::terminal_color::yellow), "- x25519: {}", tools::type_to_hex(keys.pub_x25519))); } else { // Only print the x25519 version because it's the only thing useful for a non-SN (for // encrypted LMQ RPC connections). oxen::log::info(logcat, fmt::format(fg(fmt::terminal_color::yellow), "x25519 public key: {}", tools::type_to_hex(keys.pub_x25519))); } return true; } oxenmq::AuthLevel core::omq_check_access(const crypto::x25519_public_key& pubkey) const { auto it = m_omq_auth.find(pubkey); if (it != m_omq_auth.end()) return it->second; return oxenmq::AuthLevel::denied; } // Builds an allow function; takes `*this`, the default auth level, and whether this connection // should allow incoming SN connections. // // default_auth should be AuthLevel::denied if only pre-approved connections may connect, // AuthLevel::basic for public RPC, AuthLevel::admin for a (presumably localhost) unrestricted // port, and AuthLevel::none for a super restricted mode (generally this is useful when there are // also SN-restrictions on commands, i.e. for quorumnet). // // check_sn is whether we check an incoming key against known service nodes (and thus return // "true" for the service node access if it checks out). // oxenmq::AuthLevel core::omq_allow(std::string_view ip, std::string_view x25519_pubkey_str, oxenmq::AuthLevel default_auth) { using namespace oxenmq; AuthLevel auth = default_auth; if (x25519_pubkey_str.size() == sizeof(crypto::x25519_public_key)) { crypto::x25519_public_key x25519_pubkey; std::memcpy(x25519_pubkey.data, x25519_pubkey_str.data(), x25519_pubkey_str.size()); auto user_auth = omq_check_access(x25519_pubkey); if (user_auth >= AuthLevel::basic) { if (user_auth > auth) auth = user_auth; oxen::log::info(oxen::log::Cat("omq"), "Incoming {}-authenticated connection", auth); } oxen::log::info(oxen::log::Cat("omq"), "Incoming [{}] curve connection from {}/{}", auth, ip, x25519_pubkey); } else { oxen::log::info(oxen::log::Cat("omq"), "Incoming [{}] plain connection from {}", auth, ip); } return auth; } void core::init_oxenmq(const boost::program_options::variables_map& vm) { using namespace oxenmq; oxen::log::info(omqlogcat, "Starting oxenmq"); m_omq = std::make_unique( tools::copy_guts(m_service_keys.pub_x25519), tools::copy_guts(m_service_keys.key_x25519), m_service_node, [this](std::string_view x25519_pk) { return m_service_node_list.remote_lookup(x25519_pk); }, [](LogLevel omqlevel, const char *file, int line, std::string msg) { auto level = *oxen::logging::parse_level(omqlevel); if(omqlogcat->should_log(level)) omqlogcat->log({file, line, "omq"}, level, "{}", msg); }, oxenmq::LogLevel::trace ); // ping.ping: a simple debugging target for pinging the omq listener m_omq->add_category("ping", Access{AuthLevel::none}) .add_request_command("ping", [](Message& m) { oxen::log::info(oxen::log::Cat("omq"), "Received ping from {}", m.conn); m.send_reply("pong"); }) ; if (m_service_node) { // Service nodes always listen for quorumnet data on the p2p IP, quorumnet port std::string listen_ip = vm["p2p-bind-ip"].as(); if (listen_ip.empty()) listen_ip = "0.0.0.0"; std::string qnet_listen = "tcp://" + listen_ip + ":" + std::to_string(m_quorumnet_port); oxen::log::info(logcat, "- listening on {} (quorumnet)", qnet_listen); m_omq->listen_curve(qnet_listen, [this, public_=command_line::get_arg(vm, arg_omq_quorumnet_public)](std::string_view ip, std::string_view pk, bool) { return omq_allow(ip, pk, public_ ? AuthLevel::basic : AuthLevel::none); }); m_quorumnet_state = quorumnet_new(*this); } quorumnet_init(*this, m_quorumnet_state); } void core::start_oxenmq() { update_omq_sns(); // Ensure we have SNs set for the current block before starting if (m_service_node) { m_pulse_thread_id = m_omq->add_tagged_thread("pulse"); m_omq->add_timer([this]() { pulse::main(m_quorumnet_state, *this); }, std::chrono::milliseconds(500), false, m_pulse_thread_id); m_omq->add_timer([this]() {this->check_service_node_time();}, 5s, false); } m_omq->start(); } //----------------------------------------------------------------------------------------------- bool core::set_genesis_block(const block& b) { return m_blockchain_storage.reset_and_set_genesis_block(b); } //----------------------------------------------------------------------------------------------- void core::deinit() { #ifdef ENABLE_SYSTEMD sd_notify(0, "STOPPING=1\nSTATUS=Shutting down"); #endif if (m_quorumnet_state) quorumnet_delete(m_quorumnet_state); m_omq.reset(); m_service_node_list.store(); m_miner.stop(); m_mempool.deinit(); m_blockchain_storage.deinit(); } //----------------------------------------------------------------------------------------------- void core::test_drop_download() { m_test_drop_download = false; } //----------------------------------------------------------------------------------------------- void core::test_drop_download_height(uint64_t height) { m_test_drop_download_height = height; } //----------------------------------------------------------------------------------------------- bool core::get_test_drop_download() const { return m_test_drop_download; } //----------------------------------------------------------------------------------------------- bool core::get_test_drop_download_height() const { if (m_test_drop_download_height == 0) return true; if (get_blockchain_storage().get_current_blockchain_height() <= m_test_drop_download_height) return true; return false; } //----------------------------------------------------------------------------------------------- void core::parse_incoming_tx_pre(tx_verification_batch_info &tx_info) { if(tx_info.blob->size() > MAX_TX_SIZE) { oxen::log::info(logcat, "WRONG TRANSACTION BLOB, too big size {}, rejected", tx_info.blob->size()); tx_info.tvc.m_verifivation_failed = true; tx_info.tvc.m_too_big = true; return; } else if (tx_info.blob->empty()) { oxen::log::info(logcat, "WRONG TRANSACTION BLOB, blob is empty, rejected"); tx_info.tvc.m_verifivation_failed = true; return; } tx_info.parsed = parse_and_validate_tx_from_blob(*tx_info.blob, tx_info.tx, tx_info.tx_hash); if(!tx_info.parsed) { oxen::log::info(logcat, "WRONG TRANSACTION BLOB, Failed to parse, rejected"); tx_info.tvc.m_verifivation_failed = true; return; } //std::cout << "!"<< tx.vin.size() << std::endl; std::lock_guard lock{bad_semantics_txes_lock}; for (int idx = 0; idx < 2; ++idx) { if (bad_semantics_txes[idx].find(tx_info.tx_hash) != bad_semantics_txes[idx].end()) { oxen::log::info(logcat, "Transaction already seen with bad semantics, rejected"); tx_info.tvc.m_verifivation_failed = true; return; } } tx_info.result = true; } //----------------------------------------------------------------------------------------------- void core::set_semantics_failed(const crypto::hash &tx_hash) { oxen::log::info(logcat, "WRONG TRANSACTION BLOB, Failed to check tx {} semantic, rejected", tx_hash); bad_semantics_txes_lock.lock(); bad_semantics_txes[0].insert(tx_hash); if (bad_semantics_txes[0].size() >= BAD_SEMANTICS_TXES_MAX_SIZE) { std::swap(bad_semantics_txes[0], bad_semantics_txes[1]); bad_semantics_txes[0].clear(); } bad_semantics_txes_lock.unlock(); } //----------------------------------------------------------------------------------------------- static bool is_canonical_bulletproof_layout(const std::vector &proofs) { if (proofs.size() != 1) return false; const size_t sz = proofs[0].V.size(); if (sz == 0 || sz > TX_BULLETPROOF_MAX_OUTPUTS) return false; return true; } //----------------------------------------------------------------------------------------------- void core::parse_incoming_tx_accumulated_batch(std::vector &tx_info, bool kept_by_block) { if (kept_by_block && get_blockchain_storage().is_within_compiled_block_hash_area()) { oxen::log::trace(logcat, "Skipping semantics check for txs kept by block in embedded hash area"); return; } std::vector rvv; for (size_t n = 0; n < tx_info.size(); ++n) { if (!tx_info[n].result || tx_info[n].already_have) continue; if (!check_tx_semantic(tx_info[n].tx, kept_by_block)) { set_semantics_failed(tx_info[n].tx_hash); tx_info[n].tvc.m_verifivation_failed = true; tx_info[n].result = false; continue; } if (!tx_info[n].tx.is_transfer()) continue; const rct::rctSig &rv = tx_info[n].tx.rct_signatures; switch (rv.type) { case rct::RCTType::Null: // coinbase should not come here, so we reject for all other types oxen::log::error(oxen::log::Cat("verify"), "Unexpected Null rctSig type"); set_semantics_failed(tx_info[n].tx_hash); tx_info[n].tvc.m_verifivation_failed = true; tx_info[n].result = false; break; case rct::RCTType::Simple: if (!rct::verRctSemanticsSimple(rv)) { oxen::log::error(oxen::log::Cat("verify"), "rct signature semantics check failed"); set_semantics_failed(tx_info[n].tx_hash); tx_info[n].tvc.m_verifivation_failed = true; tx_info[n].result = false; break; } break; case rct::RCTType::Full: if (!rct::verRct(rv, true)) { oxen::log::error(oxen::log::Cat("verify"), "rct signature semantics check failed"); set_semantics_failed(tx_info[n].tx_hash); tx_info[n].tvc.m_verifivation_failed = true; tx_info[n].result = false; break; } break; case rct::RCTType::Bulletproof: case rct::RCTType::Bulletproof2: case rct::RCTType::CLSAG: if (!is_canonical_bulletproof_layout(rv.p.bulletproofs)) { oxen::log::error(oxen::log::Cat("verify"), "Bulletproof does not have canonical form"); set_semantics_failed(tx_info[n].tx_hash); tx_info[n].tvc.m_verifivation_failed = true; tx_info[n].result = false; break; } rvv.push_back(&rv); // delayed batch verification break; default: oxen::log::error(oxen::log::Cat("verify"), "Unknown rct type: {}", (int)rv.type); set_semantics_failed(tx_info[n].tx_hash); tx_info[n].tvc.m_verifivation_failed = true; tx_info[n].result = false; break; } } if (!rvv.empty() && !rct::verRctSemanticsSimple(rvv)) { oxen::log::info(logcat, "One transaction among this group has bad semantics, verifying one at a time"); const bool assumed_bad = rvv.size() == 1; // if there's only one tx, it must be the bad one for (size_t n = 0; n < tx_info.size(); ++n) { if (!tx_info[n].result || tx_info[n].already_have) continue; if (!rct::is_rct_bulletproof(tx_info[n].tx.rct_signatures.type)) continue; if (assumed_bad || !rct::verRctSemanticsSimple(tx_info[n].tx.rct_signatures)) { set_semantics_failed(tx_info[n].tx_hash); tx_info[n].tvc.m_verifivation_failed = true; tx_info[n].result = false; } } } } //----------------------------------------------------------------------------------------------- std::vector core::parse_incoming_txs(const std::vector& tx_blobs, const tx_pool_options &opts) { // Caller needs to do this around both this *and* handle_parsed_txs //auto lock = incoming_tx_lock(); std::vector tx_info(tx_blobs.size()); tools::threadpool& tpool = tools::threadpool::getInstance(); tools::threadpool::waiter waiter; for (size_t i = 0; i < tx_blobs.size(); i++) { tx_info[i].blob = &tx_blobs[i]; tpool.submit(&waiter, [this, &info = tx_info[i]] { try { parse_incoming_tx_pre(info); } catch (const std::exception &e) { oxen::log::error(oxen::log::Cat("verify"), "Exception in handle_incoming_tx_pre: {}", e.what()); info.tvc.m_verifivation_failed = true; } }); } waiter.wait(&tpool); for (auto &info : tx_info) { if (!info.result) continue; if(m_mempool.have_tx(info.tx_hash)) { oxen::log::debug(logcat, "tx {} already has a transaction in tx_pool", info.tx_hash); info.already_have = true; } else if(m_blockchain_storage.have_tx(info.tx_hash)) { oxen::log::debug(logcat, "tx {} already has a transaction in tx_pool", info.tx_hash); info.already_have = true; } } parse_incoming_tx_accumulated_batch(tx_info, opts.kept_by_block); return tx_info; } bool core::handle_parsed_txs(std::vector &parsed_txs, const tx_pool_options &opts, uint64_t *blink_rollback_height) { // Caller needs to do this around both this *and* parse_incoming_txs //auto lock = incoming_tx_lock(); auto version = m_blockchain_storage.get_network_version(); bool ok = true; if (blink_rollback_height) *blink_rollback_height = 0; tx_pool_options tx_opts; for (size_t i = 0; i < parsed_txs.size(); i++) { auto &info = parsed_txs[i]; if (!info.result) { ok = false; // Propagate failures (so this can be chained with parse_incoming_txs without an intermediate check) continue; } if (opts.kept_by_block) get_blockchain_storage().on_new_tx_from_block(info.tx); if (info.already_have) continue; // Not a failure const size_t weight = get_transaction_weight(info.tx, info.blob->size()); const tx_pool_options *local_opts = &opts; if (blink_rollback_height && info.approved_blink) { // If this is an approved blink then pass a copy of the options with the flag added tx_opts = opts; tx_opts.approved_blink = true; local_opts = &tx_opts; } if (m_mempool.add_tx(info.tx, info.tx_hash, *info.blob, weight, info.tvc, *local_opts, version, blink_rollback_height)) { oxen::log::debug(logcat, "tx added: {}", info.tx_hash); } else { ok = false; if (info.tvc.m_verifivation_failed) oxen::log::error(oxen::log::Cat("verify"), "Transaction verification failed: {}", info.tx_hash); else if (info.tvc.m_verifivation_impossible) oxen::log::error(oxen::log::Cat("verify"), "Transaction verification impossible: {}", info.tx_hash); } } return ok; } //----------------------------------------------------------------------------------------------- std::vector core::handle_incoming_txs(const std::vector& tx_blobs, const tx_pool_options &opts) { auto lock = incoming_tx_lock(); auto parsed = parse_incoming_txs(tx_blobs, opts); handle_parsed_txs(parsed, opts); return parsed; } //----------------------------------------------------------------------------------------------- bool core::handle_incoming_tx(const std::string& tx_blob, tx_verification_context& tvc, const tx_pool_options &opts) { const std::vector tx_blobs{{tx_blob}}; auto parsed = handle_incoming_txs(tx_blobs, opts); parsed[0].blob = &tx_blob; // Update pointer to the input rather than the copy in case the caller wants to use it for some reason tvc = parsed[0].tvc; return parsed[0].result && (parsed[0].already_have || tvc.m_added_to_pool); } //----------------------------------------------------------------------------------------------- std::pair>, std::unordered_set> core::parse_incoming_blinks(const std::vector &blinks) { std::pair>, std::unordered_set> results; auto &new_blinks = results.first; auto &missing_txs = results.second; if (m_blockchain_storage.get_network_version() < feature::BLINK) return results; std::vector want(blinks.size(), false); // Really bools, but std::vector is broken. size_t want_count = 0; // Step 1: figure out which referenced transactions we want to keep: // - unknown tx (typically an incoming blink) // - in mempool without blink sigs (it's possible to get the tx before the blink signatures) // - in a recent, still-mutable block with blink sigs (can happen when syncing blocks before // retrieving blink signatures) { std::vector hashes; hashes.reserve(blinks.size()); for (auto &bm : blinks) hashes.emplace_back(bm.tx_hash); std::unique_lock lock(m_blockchain_storage); auto tx_block_heights = m_blockchain_storage.get_transactions_heights(hashes); auto immutable_height = m_blockchain_storage.get_immutable_height(); auto &db = m_blockchain_storage.get_db(); for (size_t i = 0; i < blinks.size(); i++) { if (tx_block_heights[i] == 0 /*mempool or unknown*/ || tx_block_heights[i] > immutable_height /*mined but not yet immutable*/) { want[i] = true; want_count++; } } } oxen::log::debug(logcat, "Want {} of {} incoming blink signature sets after filtering out immutable txes", want_count, blinks.size()); if (!want_count) return results; // Step 2: filter out any transactions for which we already have a blink signature { auto mempool_lock = m_mempool.blink_shared_lock(); for (size_t i = 0; i < blinks.size(); i++) { if (want[i] && m_mempool.has_blink(blinks[i].tx_hash)) { oxen::log::debug(logcat, "Ignoring blink data for {}: already have blink signatures", blinks[i].tx_hash); want[i] = false; // Already have it, move along want_count--; } } } oxen::log::debug(logcat, "Want {} of {} incoming blink signature sets after filtering out existing blink sigs", want_count, blinks.size()); if (!want_count) return results; // Step 3: create new blink_tx objects for txes and add the blink signatures. We can do all of // this without a lock since these are (for now) just local instances. new_blinks.reserve(want_count); std::unordered_map> quorum_cache; for (size_t i = 0; i < blinks.size(); i++) { if (!want[i]) continue; auto &bdata = blinks[i]; new_blinks.push_back(std::make_shared(bdata.height, bdata.tx_hash)); auto &blink = *new_blinks.back(); // Data structure checks (we have more stringent checks for validity later, but if these fail // now then there's no point of even trying to do signature validation. if (bdata.signature.size() != bdata.position.size() || // Each signature must have an associated quorum position bdata.signature.size() != bdata.quorum.size() || // and quorum index bdata.signature.size() < service_nodes::BLINK_MIN_VOTES * tools::enum_count || // too few signatures for possible validity bdata.signature.size() > service_nodes::BLINK_SUBQUORUM_SIZE * tools::enum_count || // too many signatures blink_tx::quorum_height(bdata.height, blink_tx::subquorum::base) == 0 || // Height is too early (no blink quorum height) std::any_of(bdata.position.begin(), bdata.position.end(), [](const auto &p) { return p >= service_nodes::BLINK_SUBQUORUM_SIZE; }) || // invalid position std::any_of(bdata.quorum.begin(), bdata.quorum.end(), [](const auto &qi) { return qi >= tools::enum_count; }) // invalid quorum index ) { oxen::log::info(logcat, "Invalid blink tx {}: invalid signature data", bdata.tx_hash); continue; } bool no_quorum = false; std::array *, tools::enum_count> validators; for (uint8_t qi = 0; qi < tools::enum_count; qi++) { auto q_height = blink.quorum_height(static_cast(qi)); auto &q = quorum_cache[q_height]; if (!q) q = get_quorum(service_nodes::quorum_type::blink, q_height); if (!q) { oxen::log::info(logcat, "Don't have a quorum for height {} (yet?), ignoring this blink", q_height); no_quorum = true; break; } validators[qi] = &q->validators; } if (no_quorum) continue; std::vector> failures; for (size_t s = 0; s < bdata.signature.size(); s++) { try { blink.add_signature(static_cast(bdata.quorum[s]), bdata.position[s], true /*approved*/, bdata.signature[s], validators[bdata.quorum[s]]->at(bdata.position[s])); } catch (const std::exception &e) { failures.emplace_back(s, e.what()); } } if (blink.approved()) { oxen::log::info(logcat, "Blink tx {} blink signatures approved with {} signature validation failures", bdata.tx_hash, failures.size()); for (auto &f : failures) oxen::log::debug(logcat, "- failure for quorum {}, position {}: {}", int(bdata.quorum[f.first]), int(bdata.position[f.first]), f.second); } else { std::ostringstream os; os << "Blink validation failed:"; for (auto &f : failures) os << " [" << int(bdata.quorum[f.first]) << ":" << int(bdata.position[f.first]) << "]: " << f.second; oxen::log::info(logcat, "Invalid blink tx {}: {}", bdata.tx_hash, os.str()); } } return results; } int core::add_blinks(const std::vector> &blinks) { int added = 0; if (blinks.empty()) return added; auto lock = m_mempool.blink_unique_lock(); for (auto &b : blinks) if (b->approved()) if (m_mempool.add_existing_blink(b)) added++; if (added) { oxen::log::info(logcat, "Added blink signatures for {} blinks", added); long_poll_trigger(m_mempool); } return added; } //----------------------------------------------------------------------------------------------- std::future> core::handle_blink_tx(const std::string &tx_blob) { return quorumnet_send_blink(*this, tx_blob); } //----------------------------------------------------------------------------------------------- bool core::check_tx_semantic(const transaction& tx, bool keeped_by_block) const { if (tx.is_transfer()) { if (tx.vin.empty()) { oxen::log::error(oxen::log::Cat("verify"), "tx with empty inputs, rejected for tx id= {}", get_transaction_hash(tx)); return false; } } else { if (tx.vin.size() != 0) { oxen::log::error(oxen::log::Cat("verify"), "tx type: {} must have 0 inputs, received: {}, rejected for tx id = {}", tx.type, tx.vin.size(), get_transaction_hash(tx)); return false; } } if(!check_inputs_types_supported(tx)) { oxen::log::error(oxen::log::Cat("verify"), "unsupported input types for tx id= {}", get_transaction_hash(tx)); return false; } if(!check_outs_valid(tx)) { oxen::log::error(oxen::log::Cat("verify"), "tx with invalid outputs, rejected for tx id= {}", get_transaction_hash(tx)); return false; } if (tx.version >= txversion::v2_ringct) { if (tx.rct_signatures.outPk.size() != tx.vout.size()) { oxen::log::error(oxen::log::Cat("verify"), "tx with mismatched vout/outPk count, rejected for tx id= {}", get_transaction_hash(tx)); return false; } } if(!check_money_overflow(tx)) { oxen::log::error(oxen::log::Cat("verify"), "tx has money overflow, rejected for tx id= {}", get_transaction_hash(tx)); return false; } if (tx.version == txversion::v1) { uint64_t amount_in = 0; get_inputs_money_amount(tx, amount_in); uint64_t amount_out = get_outs_money_amount(tx); if(amount_in <= amount_out) { oxen::log::error(oxen::log::Cat("verify"), "tx with wrong amounts: ins {}, outs {}, rejected for tx id= {}", amount_in, amount_out, get_transaction_hash(tx)); return false; } } if(!keeped_by_block && get_transaction_weight(tx) >= m_blockchain_storage.get_current_cumulative_block_weight_limit() - COINBASE_BLOB_RESERVED_SIZE) { oxen::log::error(oxen::log::Cat("verify"), "tx is too large {}, expected not bigger than {}", get_transaction_weight(tx), m_blockchain_storage.get_current_cumulative_block_weight_limit() - COINBASE_BLOB_RESERVED_SIZE); return false; } if(!check_tx_inputs_keyimages_diff(tx)) { oxen::log::error(oxen::log::Cat("verify"), "tx uses a single key image more than once"); return false; } if (!check_tx_inputs_ring_members_diff(tx)) { oxen::log::error(oxen::log::Cat("verify"), "tx uses duplicate ring members"); return false; } if (!check_tx_inputs_keyimages_domain(tx)) { oxen::log::error(oxen::log::Cat("verify"), "tx uses key image not in the valid domain"); return false; } return true; } //----------------------------------------------------------------------------------------------- bool core::check_service_node_time() { if(!is_active_sn()) {return true;} crypto::public_key pubkey = m_service_node_list.get_random_pubkey(); crypto::x25519_public_key x_pkey{0}; constexpr std::array MIN_TIMESTAMP_VERSION{9,1,0}; std::array proofversion; m_service_node_list.access_proof(pubkey, [&](auto &proof) { x_pkey = proof.pubkey_x25519; proofversion = proof.proof->version; }); if (proofversion >= MIN_TIMESTAMP_VERSION && x_pkey) { m_omq->request( tools::view_guts(x_pkey), "quorum.timestamp", [this, pubkey](bool success, std::vector data) { const time_t local_seconds = time(nullptr); oxen::log::debug(logcat, "Timestamp message received: {}, local time is: ", data[0], local_seconds); if(success){ int64_t received_seconds; if (tools::parse_int(data[0],received_seconds)){ uint16_t variance; if (received_seconds > local_seconds + 65535 || received_seconds < local_seconds - 65535) { variance = 65535; } else { variance = std::abs(local_seconds - received_seconds); } std::lock_guard lk(m_sn_timestamp_mutex); // Records the variance into the record of our performance (m_sn_times) service_nodes::timesync_entry entry{variance <= service_nodes::THRESHOLD_SECONDS_OUT_OF_SYNC}; m_sn_times.add(entry); // Counts the number of times we have been out of sync if (m_sn_times.failures() > (m_sn_times.size() * service_nodes::MAXIMUM_EXTERNAL_OUT_OF_SYNC/100)) { oxen::log::warning(logcat, "service node time might be out of sync"); // If we are out of sync record the other service node as in sync m_service_node_list.record_timesync_status(pubkey, true); } else { m_service_node_list.record_timesync_status(pubkey, variance <= service_nodes::THRESHOLD_SECONDS_OUT_OF_SYNC); } } else { success = false; } } m_service_node_list.record_timestamp_participation(pubkey, success); }); } return true; } //----------------------------------------------------------------------------------------------- bool core::is_key_image_spent(const crypto::key_image &key_image) const { return m_blockchain_storage.have_tx_keyimg_as_spent(key_image); } //----------------------------------------------------------------------------------------------- bool core::are_key_images_spent(const std::vector& key_im, std::vector &spent) const { spent.clear(); for(auto& ki: key_im) { spent.push_back(m_blockchain_storage.have_tx_keyimg_as_spent(ki)); } return true; } //----------------------------------------------------------------------------------------------- size_t core::get_block_sync_size(uint64_t height) const { return block_sync_size > 0 ? block_sync_size : BLOCKS_SYNCHRONIZING_DEFAULT_COUNT; } //----------------------------------------------------------------------------------------------- bool core::are_key_images_spent_in_pool(const std::vector& key_im, std::vector &spent) const { spent.clear(); return m_mempool.check_for_key_images(key_im, spent); } //----------------------------------------------------------------------------------------------- std::optional> core::get_coinbase_tx_sum(uint64_t start_offset, size_t count) { std::optional> result{{0, 0, 0}}; if (count == 0) return result; auto& [emission_amount, total_fee_amount, burnt_oxen] = *result; // Caching. // // Requesting this value from the beginning of the chain is very slow, so we cache it. That // still means the first request will be slow, but that's okay. To prevent a bunch of threads // getting backed up trying to calculate this, we lock out more than one thread building the // cache at a time if we're requesting a large number of block values at once. Any other thread // requesting will get a nullopt back. constexpr uint64_t CACHE_LAG = 30; // We cache the values up to this many blocks ago; we lag so that we don't have to worry about small reorgs constexpr uint64_t CACHE_EXCLUSIVE = 1000; // If we need to load more than this, we block out other threads // Check if we have a cacheable from-the-beginning result uint64_t cache_to = 0; std::chrono::steady_clock::time_point cache_build_started; if (start_offset == 0) { uint64_t height = m_blockchain_storage.get_current_blockchain_height(); if (count > height) count = height; cache_to = height - std::min(CACHE_LAG, height); { std::shared_lock lock{m_coinbase_cache.mutex}; if (m_coinbase_cache.height && count >= m_coinbase_cache.height) { emission_amount = m_coinbase_cache.emissions; total_fee_amount = m_coinbase_cache.fees; burnt_oxen = m_coinbase_cache.burnt; start_offset = m_coinbase_cache.height + 1; count -= m_coinbase_cache.height; } // else don't change anything; we need a subset of blocks that ends before the cache. if (cache_to <= m_coinbase_cache.height) cache_to = 0; // Cache doesn't need updating } // If we're loading a lot then acquire an exclusive lock, recheck our variables, and block out // other threads until we're done. (We don't do this if we're only loading a few because even // if we have some competing cache updates they don't hurt anything). if (cache_to > 0 && count > CACHE_EXCLUSIVE) { std::unique_lock lock{m_coinbase_cache.mutex}; if (m_coinbase_cache.building) return std::nullopt; // Another thread is already updating the cache if (m_coinbase_cache.height && m_coinbase_cache.height >= start_offset) { // Someone else updated the cache while we were acquiring the unique lock, so update our variables if (m_coinbase_cache.height >= start_offset + count) { // The cache is now *beyond* us, which means we can't use it, so reset start/count back // to what they were originally. count += start_offset - 1; start_offset = 0; cache_to = 0; } else { // The cache is updated and we can still use it, so update our variables. emission_amount = m_coinbase_cache.emissions; total_fee_amount = m_coinbase_cache.fees; burnt_oxen = m_coinbase_cache.burnt; count -= m_coinbase_cache.height - start_offset + 1; start_offset = m_coinbase_cache.height + 1; } } if (cache_to > 0 && count > CACHE_EXCLUSIVE) { cache_build_started = std::chrono::steady_clock::now(); m_coinbase_cache.building = true; // Block out other threads until we're done oxen::log::info(logcat, "Starting slow cache build request for get_coinbase_tx_sum({}, {})", start_offset, count); } } } const uint64_t end = start_offset + count - 1; m_blockchain_storage.for_blocks_range(start_offset, end, [this, &cache_to, &result, &cache_build_started](uint64_t height, const crypto::hash& hash, const block& b){ auto& [emission_amount, total_fee_amount, burnt_oxen] = *result; std::vector txs; auto coinbase_amount = static_cast(get_outs_money_amount(b.miner_tx)); get_transactions(b.tx_hashes, txs); int64_t tx_fee_amount = 0; for(const auto& tx: txs) { tx_fee_amount += static_cast(get_tx_miner_fee(tx, b.major_version >= feature::FEE_BURNING)); if(b.major_version >= feature::FEE_BURNING) { burnt_oxen += static_cast(get_burned_amount_from_tx_extra(tx.extra)); } } emission_amount += coinbase_amount - tx_fee_amount; total_fee_amount += tx_fee_amount; if (cache_to && cache_to == height) { std::unique_lock lock{m_coinbase_cache.mutex}; if (m_coinbase_cache.height < height) { m_coinbase_cache.height = height; m_coinbase_cache.emissions = emission_amount; m_coinbase_cache.fees = total_fee_amount; m_coinbase_cache.burnt = burnt_oxen; } if (m_coinbase_cache.building) { m_coinbase_cache.building = false; oxen::log::info(logcat, "Finishing cache build for get_coinbase_tx_sum in {} s", std::chrono::duration{std::chrono::steady_clock::now() - cache_build_started}.count()); } cache_to = 0; } return true; }); return result; } //----------------------------------------------------------------------------------------------- bool core::check_tx_inputs_keyimages_diff(const transaction& tx) const { std::unordered_set ki; for(const auto& in: tx.vin) { CHECKED_GET_SPECIFIC_VARIANT(in, txin_to_key, tokey_in, false); if(!ki.insert(tokey_in.k_image).second) return false; } return true; } //----------------------------------------------------------------------------------------------- bool core::check_tx_inputs_ring_members_diff(const transaction& tx) const { const auto version = m_blockchain_storage.get_network_version(); for(const auto& in: tx.vin) { CHECKED_GET_SPECIFIC_VARIANT(in, txin_to_key, tokey_in, false); for (size_t n = 1; n < tokey_in.key_offsets.size(); ++n) if (tokey_in.key_offsets[n] == 0) return false; } return true; } //----------------------------------------------------------------------------------------------- bool core::check_tx_inputs_keyimages_domain(const transaction& tx) const { std::unordered_set ki; for(const auto& in: tx.vin) { CHECKED_GET_SPECIFIC_VARIANT(in, txin_to_key, tokey_in, false); if (!(rct::scalarmultKey(rct::ki2rct(tokey_in.k_image), rct::curveOrder()) == rct::identity())) return false; } return true; } //----------------------------------------------------------------------------------------------- size_t core::get_blockchain_total_transactions() const { return m_blockchain_storage.get_total_transactions(); } //----------------------------------------------------------------------------------------------- bool core::relay_txpool_transactions() { // we attempt to relay txes that should be relayed, but were not std::vector> txs; if (m_mempool.get_relayable_transactions(txs) && !txs.empty()) { cryptonote_connection_context fake_context{}; tx_verification_context tvc{}; NOTIFY_NEW_TRANSACTIONS::request r{}; for (auto it = txs.begin(); it != txs.end(); ++it) { r.txs.push_back(it->second); } get_protocol()->relay_transactions(r, fake_context); m_mempool.set_relayed(txs); } return true; } //----------------------------------------------------------------------------------------------- bool core::submit_uptime_proof() { if (!m_service_node) return true; cryptonote_connection_context fake_context{}; bool relayed; auto height = get_current_blockchain_height(); auto proof = m_service_node_list.generate_uptime_proof(m_sn_public_ip, storage_https_port(), storage_omq_port(), ss_version, m_quorumnet_port, lokinet_version); NOTIFY_BTENCODED_UPTIME_PROOF::request req = proof.generate_request(); relayed = get_protocol()->relay_btencoded_uptime_proof(req, fake_context); // TODO: remove after HF19 if (relayed && tools::view_guts(m_service_keys.pub) != tools::view_guts(m_service_keys.pub_ed25519)) { // Temp workaround: nodes with both pub and ed25519 are failing bt-encoded proofs, so send // an old-style proof out as well as a workaround. NOTIFY_UPTIME_PROOF::request req = m_service_node_list.generate_uptime_proof(m_sn_public_ip, storage_https_port(), storage_omq_port(), m_quorumnet_port); get_protocol()->relay_uptime_proof(req, fake_context); } if (relayed) oxen::log::info(logcat, "Submitted uptime-proof for Service Node (yours): {}", m_service_keys.pub); return true; } //----------------------------------------------------------------------------------------------- bool core::handle_uptime_proof(const NOTIFY_UPTIME_PROOF::request &proof, bool &my_uptime_proof_confirmation) { crypto::x25519_public_key pkey = {}; bool result = m_service_node_list.handle_uptime_proof(proof, my_uptime_proof_confirmation, pkey); if (result && m_service_node_list.is_service_node(proof.pubkey, true /*require_active*/) && pkey) { oxenmq::pubkey_set added; added.insert(tools::copy_guts(pkey)); m_omq->update_active_sns(added, {} /*removed*/); } return result; } //----------------------------------------------------------------------------------------------- bool core::handle_btencoded_uptime_proof(const NOTIFY_BTENCODED_UPTIME_PROOF::request &req, bool &my_uptime_proof_confirmation) { crypto::x25519_public_key pkey = {}; auto proof = std::make_unique(req.proof); proof->sig = tools::make_from_guts(req.sig); proof->sig_ed25519 = tools::make_from_guts(req.ed_sig); auto pubkey = proof->pubkey; bool result = m_service_node_list.handle_btencoded_uptime_proof(std::move(proof), my_uptime_proof_confirmation, pkey); if (result && m_service_node_list.is_service_node(pubkey, true /*require_active*/) && pkey) { oxenmq::pubkey_set added; added.insert(tools::copy_guts(pkey)); m_omq->update_active_sns(added, {} /*removed*/); } return result; } //----------------------------------------------------------------------------------------------- crypto::hash core::on_transaction_relayed(const std::string& tx_blob) { std::vector> txs; cryptonote::transaction tx; crypto::hash tx_hash; if (!parse_and_validate_tx_from_blob(tx_blob, tx, tx_hash)) { oxen::log::error(logcat, "Failed to parse relayed transaction"); return crypto::null_hash; } txs.push_back(std::make_pair(tx_hash, std::move(tx_blob))); m_mempool.set_relayed(txs); return tx_hash; } //----------------------------------------------------------------------------------------------- bool core::relay_service_node_votes() { auto height = get_current_blockchain_height(); auto hf_version = get_network_version(m_nettype, height); auto quorum_votes = m_quorum_cop.get_relayable_votes(height, hf_version, true); auto p2p_votes = m_quorum_cop.get_relayable_votes(height, hf_version, false); if (!quorum_votes.empty() && m_quorumnet_state && m_service_node) quorumnet_relay_obligation_votes(m_quorumnet_state, quorum_votes); if (!p2p_votes.empty()) { NOTIFY_NEW_SERVICE_NODE_VOTE::request req{}; req.votes = std::move(p2p_votes); cryptonote_connection_context fake_context{}; get_protocol()->relay_service_node_votes(req, fake_context); } return true; } void core::set_service_node_votes_relayed(const std::vector &votes) { m_quorum_cop.set_votes_relayed(votes); } //----------------------------------------------------------------------------------------------- bool core::create_next_miner_block_template(block& b, const account_public_address& adr, difficulty_type& diffic, uint64_t& height, uint64_t& expected_reward, const std::string& ex_nonce) { return m_blockchain_storage.create_next_miner_block_template(b, adr, diffic, height, expected_reward, ex_nonce); } //----------------------------------------------------------------------------------------------- bool core::create_miner_block_template(block& b, const crypto::hash *prev_block, const account_public_address& adr, difficulty_type& diffic, uint64_t& height, uint64_t& expected_reward, const std::string& ex_nonce) { return m_blockchain_storage.create_miner_block_template(b, prev_block, adr, diffic, height, expected_reward, ex_nonce); } //----------------------------------------------------------------------------------------------- bool core::find_blockchain_supplement(const std::list& qblock_ids, NOTIFY_RESPONSE_CHAIN_ENTRY::request& resp) const { return m_blockchain_storage.find_blockchain_supplement(qblock_ids, resp); } //----------------------------------------------------------------------------------------------- bool core::find_blockchain_supplement(const uint64_t req_start_block, const std::list& qblock_ids, std::vector, std::vector > > >& blocks, uint64_t& total_height, uint64_t& start_height, bool pruned, bool get_miner_tx_hash, size_t max_count) const { return m_blockchain_storage.find_blockchain_supplement(req_start_block, qblock_ids, blocks, total_height, start_height, pruned, get_miner_tx_hash, max_count); } //----------------------------------------------------------------------------------------------- bool core::get_outs(const rpc::GET_OUTPUTS_BIN::request& req, rpc::GET_OUTPUTS_BIN::response& res) const { return m_blockchain_storage.get_outs(req, res); } //----------------------------------------------------------------------------------------------- bool core::get_output_distribution(uint64_t amount, uint64_t from_height, uint64_t to_height, uint64_t &start_height, std::vector &distribution, uint64_t &base) const { return m_blockchain_storage.get_output_distribution(amount, from_height, to_height, start_height, distribution, base); } //----------------------------------------------------------------------------------------------- void core::get_output_blacklist(std::vector &blacklist) const { m_blockchain_storage.get_output_blacklist(blacklist); } //----------------------------------------------------------------------------------------------- bool core::get_tx_outputs_gindexs(const crypto::hash& tx_id, std::vector& indexs) const { return m_blockchain_storage.get_tx_outputs_gindexs(tx_id, indexs); } //----------------------------------------------------------------------------------------------- bool core::get_tx_outputs_gindexs(const crypto::hash& tx_id, size_t n_txes, std::vector>& indexs) const { return m_blockchain_storage.get_tx_outputs_gindexs(tx_id, n_txes, indexs); } //----------------------------------------------------------------------------------------------- void core::pause_mine() { m_miner.pause(); } //----------------------------------------------------------------------------------------------- void core::resume_mine() { m_miner.resume(); } //----------------------------------------------------------------------------------------------- block_complete_entry get_block_complete_entry(block& b, tx_memory_pool &pool) { block_complete_entry bce = {}; bce.block = cryptonote::block_to_blob(b); for (const auto &tx_hash: b.tx_hashes) { std::string txblob; CHECK_AND_ASSERT_THROW_MES(pool.get_transaction(tx_hash, txblob), "Transaction not found in pool"); bce.txs.push_back(txblob); } return bce; } //----------------------------------------------------------------------------------------------- bool core::handle_block_found(block& b, block_verification_context &bvc) { bvc = {}; std::vector blocks; m_miner.pause(); { OXEN_DEFER { m_miner.resume(); }; try { blocks.push_back(get_block_complete_entry(b, m_mempool)); } catch (const std::exception &e) { return false; } std::vector pblocks; if (!prepare_handle_incoming_blocks(blocks, pblocks)) { oxen::log::error(logcat, "Block found, but failed to prepare to add"); return false; } // add_new_block will verify block and set bvc.m_verification_failed accordingly add_new_block(b, bvc, nullptr /*checkpoint*/); cleanup_handle_incoming_blocks(true); m_miner.on_block_chain_update(); } if (bvc.m_verifivation_failed) { bool pulse = cryptonote::block_has_pulse_components(b); oxen::log::error(oxen::log::Cat("verify"), "{} block failed verification\n{}", (pulse ? "Pulse" : "Mined"), cryptonote::obj_to_json_str(b)); return false; } else if(bvc.m_added_to_main_chain) { std::unordered_set missed_txs; std::vector txs; m_blockchain_storage.get_transactions_blobs(b.tx_hashes, txs, &missed_txs); if(missed_txs.size() && m_blockchain_storage.get_block_id_by_height(get_block_height(b)) != get_block_hash(b)) { oxen::log::info(logcat, "Block found but, seems that reorganize just happened after that, do not relay this block"); return true; } CHECK_AND_ASSERT_MES(txs.size() == b.tx_hashes.size() && !missed_txs.size(), false, "can't find some transactions in found block:" << get_block_hash(b) << " txs.size()=" << txs.size() << ", b.tx_hashes.size()=" << b.tx_hashes.size() << ", missed_txs.size()" << missed_txs.size()); cryptonote_connection_context exclude_context{}; NOTIFY_NEW_FLUFFY_BLOCK::request arg{}; arg.current_blockchain_height = m_blockchain_storage.get_current_blockchain_height(); arg.b = blocks[0]; m_pprotocol->relay_block(arg, exclude_context); } return true; } //----------------------------------------------------------------------------------------------- void core::on_synchronized() { m_miner.on_synchronized(); } //----------------------------------------------------------------------------------------------- void core::safesyncmode(const bool onoff) { m_blockchain_storage.safesyncmode(onoff); } //----------------------------------------------------------------------------------------------- bool core::add_new_block(const block& b, block_verification_context& bvc, checkpoint_t const *checkpoint) { bool result = m_blockchain_storage.add_new_block(b, bvc, checkpoint); if (result) relay_service_node_votes(); // NOTE: nop if synchronising due to not accepting votes whilst syncing return result; } //----------------------------------------------------------------------------------------------- bool core::prepare_handle_incoming_blocks(const std::vector &blocks_entry, std::vector &blocks) { m_incoming_tx_lock.lock(); if (!m_blockchain_storage.prepare_handle_incoming_blocks(blocks_entry, blocks)) { cleanup_handle_incoming_blocks(false); return false; } return true; } //----------------------------------------------------------------------------------------------- bool core::cleanup_handle_incoming_blocks(bool force_sync) { bool success = false; try { success = m_blockchain_storage.cleanup_handle_incoming_blocks(force_sync); } catch (...) {} m_incoming_tx_lock.unlock(); return success; } //----------------------------------------------------------------------------------------------- bool core::handle_incoming_block(const std::string& block_blob, const block *b, block_verification_context& bvc, checkpoint_t *checkpoint, bool update_miner_blocktemplate) { TRY_ENTRY(); bvc = {}; if (!check_incoming_block_size(block_blob)) { bvc.m_verifivation_failed = true; return false; } if (((size_t)-1) <= 0xffffffff && block_blob.size() >= 0x3fffffff) oxen::log::warning(logcat, "This block's size is {}, closing on the 32 bit limit", block_blob.size()); CHECK_AND_ASSERT_MES(update_checkpoints_from_json_file(), false, "One or more checkpoints loaded from json conflicted with existing checkpoints."); block lb; if (!b) { crypto::hash block_hash; if(!parse_and_validate_block_from_blob(block_blob, lb, block_hash)) { oxen::log::info(logcat, "Failed to parse and validate new block"); bvc.m_verifivation_failed = true; return false; } b = &lb; } add_new_block(*b, bvc, checkpoint); if(update_miner_blocktemplate && bvc.m_added_to_main_chain) m_miner.on_block_chain_update(); return true; CATCH_ENTRY_L0("core::handle_incoming_block()", false); } //----------------------------------------------------------------------------------------------- // Used by the RPC server to check the size of an incoming // block_blob bool core::check_incoming_block_size(const std::string& block_blob) const { // note: we assume block weight is always >= block blob size, so we check incoming // blob size against the block weight limit, which acts as a sanity check without // having to parse/weigh first; in fact, since the block blob is the block header // plus the tx hashes, the weight will typically be much larger than the blob size if(block_blob.size() > m_blockchain_storage.get_current_cumulative_block_weight_limit() + BLOCK_SIZE_SANITY_LEEWAY) { oxen::log::info(logcat, "WRONG BLOCK BLOB, sanity check failed on size {}, rejected", block_blob.size()); return false; } return true; } void core::update_omq_sns() { // TODO: let callers (e.g. lokinet, ss) subscribe to callbacks when this fires oxenmq::pubkey_set active_sns; m_service_node_list.copy_active_x25519_pubkeys(std::inserter(active_sns, active_sns.end())); m_omq->set_active_sns(std::move(active_sns)); } //----------------------------------------------------------------------------------------------- crypto::hash core::get_tail_id() const { return m_blockchain_storage.get_tail_id(); } //----------------------------------------------------------------------------------------------- difficulty_type core::get_block_cumulative_difficulty(uint64_t height) const { return m_blockchain_storage.get_db().get_block_cumulative_difficulty(height); } //----------------------------------------------------------------------------------------------- bool core::have_block(const crypto::hash& id) const { return m_blockchain_storage.have_block(id); } //----------------------------------------------------------------------------------------------- crypto::hash core::get_block_id_by_height(uint64_t height) const { return m_blockchain_storage.get_block_id_by_height(height); } //----------------------------------------------------------------------------------------------- bool core::get_block_by_hash(const crypto::hash &h, block &blk, bool *orphan) const { return m_blockchain_storage.get_block_by_hash(h, blk, orphan); } //----------------------------------------------------------------------------------------------- bool core::get_block_by_height(uint64_t height, block &blk) const { return m_blockchain_storage.get_block_by_height(height, blk); } //----------------------------------------------------------------------------------------------- static bool check_external_ping(time_t last_ping, std::chrono::seconds lifetime, std::string_view what) { const std::chrono::seconds elapsed{std::time(nullptr) - last_ping}; if (elapsed > lifetime) { oxen::log::warning(logcat, "Have not heard from {} {}", what, (!last_ping ? "since starting" : "since more than " + tools::get_human_readable_timespan(elapsed) + " ago")); return false; } return true; } void core::reset_proof_interval() { m_check_uptime_proof_interval.reset(); } //----------------------------------------------------------------------------------------------- void core::do_uptime_proof_call() { std::vector const states = get_service_node_list_state({ m_service_keys.pub }); // wait one block before starting uptime proofs (but not on testnet/devnet, where we sometimes // have mass registrations/deregistrations where the waiting causes problems). uint64_t delay_blocks = m_nettype == network_type::MAINNET ? 1 : 0; if (!states.empty() && (states[0].info->registration_height + delay_blocks) < get_current_blockchain_height()) { m_check_uptime_proof_interval.do_call([this]() { // This timer is not perfectly precise and can leak seconds slightly, so send the uptime // proof if we are within half a tick of the target time. (Essentially our target proof // window becomes the first time this triggers in the 59.75-60.25 minute window). uint64_t next_proof_time = 0; m_service_node_list.access_proof(m_service_keys.pub, [&](auto &proof) { next_proof_time = proof.timestamp; }); auto& netconf = get_net_config(); next_proof_time += std::chrono::seconds{ netconf.UPTIME_PROOF_FREQUENCY - netconf.UPTIME_PROOF_CHECK_INTERVAL/2}.count(); if ((uint64_t) std::time(nullptr) < next_proof_time) return; auto pubkey = m_service_node_list.get_pubkey_from_x25519(m_service_keys.pub_x25519); if (pubkey != crypto::null_pkey && pubkey != m_service_keys.pub && m_service_node_list.is_service_node(pubkey, false /*don't require active*/)) { oxen::log::info(logcat, fmt::format(fg(fmt::terminal_color::red), "Failed to submit uptime proof: another service node on the network is using the same ed/x25519 keys as this service node. This typically means both have the same 'key_ed25519' private key file.")); return; } { std::vector sn_pks; auto sns = m_service_node_list.get_service_node_list_state(); sn_pks.reserve(sns.size()); for (const auto& sni : sns) sn_pks.push_back(sni.pubkey); m_service_node_list.for_each_service_node_info_and_proof(sn_pks.begin(), sn_pks.end(), [&](auto& pk, auto& sni, auto& proof) { if (pk != m_service_keys.pub && proof.proof->public_ip == m_sn_public_ip && (proof.proof->qnet_port == m_quorumnet_port || ( m_nettype != network_type::DEVNET && (proof.proof->storage_https_port == storage_https_port() || proof.proof->storage_omq_port == storage_omq_port())))) oxen::log::info(logcat, fmt::format(fg(fmt::terminal_color::red), "Another service node ({}) is broadcasting the same public IP and ports as this service node ({}:{}[qnet], :{}[SS-HTTP], :{}[SS-LMQ]). This will lead to deregistration of one or both service nodes if not corrected. (Do both service nodes have the correct IP for the service-node-public-ip setting?)", pk, epee::string_tools::get_ip_string_from_int32(m_sn_public_ip), proof.proof->qnet_port, proof.proof->storage_https_port, proof.proof->storage_omq_port)); }); } if (m_nettype != network_type::DEVNET) { if (!check_external_ping(m_last_storage_server_ping, get_net_config().UPTIME_PROOF_FREQUENCY, "the storage server")) { oxen::log::info(logcat, fmt::format(fg(fmt::terminal_color::red), "Failed to submit uptime proof: have not heard from the storage server recently. Make sure that it is running! It is required to run alongside the Loki daemon")); return; } if (!check_external_ping(m_last_lokinet_ping, get_net_config().UPTIME_PROOF_FREQUENCY, "Lokinet")) { oxen::log::info(logcat, fmt::format(fg(fmt::terminal_color::red), "Failed to submit uptime proof: have not heard from lokinet recently. Make sure that it is running! It is required to run alongside the Loki daemon")); return; } } submit_uptime_proof(); }); } else { // reset the interval so that we're ready when we register, OR if we get deregistered this primes us up for re-registration in the same session m_check_uptime_proof_interval.reset(); } } //----------------------------------------------------------------------------------------------- bool core::on_idle() { if(!m_starter_message_showed) { std::string main_message; if (m_offline) main_message = "The daemon is running offline and will not attempt to sync to the Loki network."; else main_message = "The daemon will start synchronizing with the network. This may take a long time to complete."; oxen::log::info(logcat, fmt::format(fg(fmt::terminal_color::yellow), "\n**********************************************************************\n\ {}\n\n\ You can set the level of process detailization through \"set_log \" command,\n\ where is between 0 (no details) and 4 (very verbose), or custom category based levels (eg, *:WARNING).\n\ \n\ Use the \"help\" command to see the list of available commands.\n\ Use \"help \" to see a command's documentation.\n\ **********************************************************************\n", main_message)); m_starter_message_showed = true; } m_txpool_auto_relayer.do_call([this] { return relay_txpool_transactions(); }); m_service_node_vote_relayer.do_call([this] { return relay_service_node_votes(); }); m_check_disk_space_interval.do_call([this] { return check_disk_space(); }); m_block_rate_interval.do_call([this] { return check_block_rate(); }); m_sn_proof_cleanup_interval.do_call([&snl=m_service_node_list] { snl.cleanup_proofs(); return true; }); std::chrono::seconds lifetime{time(nullptr) - get_start_time()}; if (m_service_node && lifetime > get_net_config().UPTIME_PROOF_STARTUP_DELAY) // Give us some time to connect to peers before sending uptimes { do_uptime_proof_call(); } m_blockchain_pruning_interval.do_call([this] { return update_blockchain_pruning(); }); m_miner.on_idle(); m_mempool.on_idle(); #ifdef ENABLE_SYSTEMD m_systemd_notify_interval.do_call([this] { sd_notify(0, ("WATCHDOG=1\nSTATUS=" + get_status_string()).c_str()); }); #endif return true; } //----------------------------------------------------------------------------------------------- bool core::check_disk_space() { uint64_t free_space = get_free_space(); if (free_space < 1ull * 1024 * 1024 * 1024) // 1 GB oxen::log::warning(logcat, fmt::format(fg(fmt::terminal_color::red), "Free space is below 1 GB on {}", m_config_folder)); return true; } //----------------------------------------------------------------------------------------------- double factorial(unsigned int n) { if (n <= 1) return 1.0; double f = n; while (n-- > 1) f *= n; return f; } //----------------------------------------------------------------------------------------------- static double probability1(unsigned int blocks, unsigned int expected) { // https://www.umass.edu/wsp/resources/poisson/#computing return pow(expected, blocks) / (factorial(blocks) * exp(expected)); } //----------------------------------------------------------------------------------------------- static double probability(unsigned int blocks, unsigned int expected) { double p = 0.0; if (blocks <= expected) { for (unsigned int b = 0; b <= blocks; ++b) p += probability1(b, expected); } else if (blocks > expected) { for (unsigned int b = blocks; b <= expected * 3 /* close enough */; ++b) p += probability1(b, expected); } return p; } //----------------------------------------------------------------------------------------------- bool core::check_block_rate() { if (m_offline || m_nettype == network_type::FAKECHAIN || m_target_blockchain_height > get_current_blockchain_height() || m_target_blockchain_height == 0) { oxen::log::debug(logcat, "Not checking block rate, offline or syncing"); return true; } static constexpr double threshold = 1. / ((24h * 10) / TARGET_BLOCK_TIME); // one false positive every 10 days static constexpr unsigned int max_blocks_checked = 150; const time_t now = time(NULL); const std::vector timestamps = m_blockchain_storage.get_last_block_timestamps(max_blocks_checked); static const unsigned int seconds[] = { 5400, 3600, 1800, 1200, 600 }; for (size_t n = 0; n < sizeof(seconds)/sizeof(seconds[0]); ++n) { unsigned int b = 0; const time_t time_boundary = now - static_cast(seconds[n]); for (time_t ts: timestamps) b += ts >= time_boundary; const double p = probability(b, seconds[n] / tools::to_seconds(TARGET_BLOCK_TIME)); oxen::log::debug(logcat, "blocks in the last {} minutes: {} (probability {})", seconds[n] / 60, b, p); if (p < threshold) { oxen::log::warning(logcat, "There were {}{} blocks in the last {} minutes, \ there might be large hash rate changes, or we might be partitioned, \ cut off from the Loki network or under attack, or your computer's time is off. \ Or it could be just sheer bad luck.", b, (b == max_blocks_checked ? " or more" : ""), seconds[n] / 60); break; // no need to look further } } return true; } //----------------------------------------------------------------------------------------------- void core::flush_bad_txs_cache() { bad_semantics_txes_lock.lock(); for (int idx = 0; idx < 2; ++idx) bad_semantics_txes[idx].clear(); bad_semantics_txes_lock.unlock(); } //----------------------------------------------------------------------------------------------- void core::flush_invalid_blocks() { m_blockchain_storage.flush_invalid_blocks(); } bool core::update_blockchain_pruning() { return m_blockchain_storage.update_blockchain_pruning(); } //----------------------------------------------------------------------------------------------- bool core::check_blockchain_pruning() { return m_blockchain_storage.check_blockchain_pruning(); } //----------------------------------------------------------------------------------------------- void core::set_target_blockchain_height(uint64_t target_blockchain_height) { m_target_blockchain_height = target_blockchain_height; } //----------------------------------------------------------------------------------------------- uint64_t core::get_target_blockchain_height() const { return m_target_blockchain_height; } //----------------------------------------------------------------------------------------------- uint64_t core::prevalidate_block_hashes(uint64_t height, const std::vector &hashes) { return get_blockchain_storage().prevalidate_block_hashes(height, hashes); } //----------------------------------------------------------------------------------------------- uint64_t core::get_free_space() const { return fs::space(m_config_folder).available; } //----------------------------------------------------------------------------------------------- std::shared_ptr core::get_quorum(service_nodes::quorum_type type, uint64_t height, bool include_old, std::vector> *alt_states) const { return m_service_node_list.get_quorum(type, height, include_old, alt_states); } //----------------------------------------------------------------------------------------------- bool core::is_service_node(const crypto::public_key& pubkey, bool require_active) const { return m_service_node_list.is_service_node(pubkey, require_active); } //----------------------------------------------------------------------------------------------- const std::vector &core::get_service_node_blacklisted_key_images() const { return m_service_node_list.get_blacklisted_key_images(); } //----------------------------------------------------------------------------------------------- std::vector core::get_service_node_list_state(const std::vector &service_node_pubkeys) const { return m_service_node_list.get_service_node_list_state(service_node_pubkeys); } //----------------------------------------------------------------------------------------------- bool core::add_service_node_vote(const service_nodes::quorum_vote_t& vote, vote_verification_context &vvc) { return m_quorum_cop.handle_vote(vote, vvc); } //----------------------------------------------------------------------------------------------- uint32_t core::get_blockchain_pruning_seed() const { return get_blockchain_storage().get_blockchain_pruning_seed(); } //----------------------------------------------------------------------------------------------- bool core::prune_blockchain(uint32_t pruning_seed) { return get_blockchain_storage().prune_blockchain(pruning_seed); } //----------------------------------------------------------------------------------------------- std::time_t core::get_start_time() const { return start_time; } //----------------------------------------------------------------------------------------------- void core::graceful_exit() { raise(SIGTERM); } }