diff --git a/src/blockchain_db/blockchain_db.h b/src/blockchain_db/blockchain_db.h index cce288793..d7230f644 100644 --- a/src/blockchain_db/blockchain_db.h +++ b/src/blockchain_db/blockchain_db.h @@ -1448,7 +1448,9 @@ public: * * @return false if the function returns false for any output, otherwise true */ - virtual bool for_all_outputs(std::function f) const = 0; + virtual bool for_all_outputs(std::function f) const = 0; + virtual bool for_all_outputs(uint64_t amount, const std::function &f) const = 0; + // diff --git a/src/blockchain_db/lmdb/db_lmdb.cpp b/src/blockchain_db/lmdb/db_lmdb.cpp index f9c829013..9a755fb0c 100644 --- a/src/blockchain_db/lmdb/db_lmdb.cpp +++ b/src/blockchain_db/lmdb/db_lmdb.cpp @@ -2564,7 +2564,7 @@ bool BlockchainLMDB::for_all_transactions(std::function f) const +bool BlockchainLMDB::for_all_outputs(std::function f) const { LOG_PRINT_L3("BlockchainLMDB::" << __func__); check_open(); @@ -2588,7 +2588,47 @@ bool BlockchainLMDB::for_all_outputs(std::functionoutput_id); - if (!f(amount, toi.first, toi.second)) { + if (!f(amount, toi.first, ok->data.height, toi.second)) { + fret = false; + break; + } + } + + TXN_POSTFIX_RDONLY(); + + return fret; +} + +bool BlockchainLMDB::for_all_outputs(uint64_t amount, const std::function &f) const +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + + TXN_PREFIX_RDONLY(); + RCURSOR(output_amounts); + + MDB_val_set(k, amount); + MDB_val v; + bool fret = true; + + MDB_cursor_op op = MDB_SET; + while (1) + { + int ret = mdb_cursor_get(m_cur_output_amounts, &k, &v, op); + op = MDB_NEXT_DUP; + if (ret == MDB_NOTFOUND) + break; + if (ret) + throw0(DB_ERROR("Failed to enumerate outputs")); + uint64_t out_amount = *(const uint64_t*)k.mv_data; + if (amount != out_amount) + { + MERROR("Amount is not the expected amount"); + fret = false; + break; + } + const outkey *ok = (const outkey *)v.mv_data; + if (!f(ok->data.height)) { fret = false; break; } diff --git a/src/blockchain_db/lmdb/db_lmdb.h b/src/blockchain_db/lmdb/db_lmdb.h index 1b76abf35..0aa4bb86e 100644 --- a/src/blockchain_db/lmdb/db_lmdb.h +++ b/src/blockchain_db/lmdb/db_lmdb.h @@ -255,7 +255,8 @@ public: virtual bool for_all_key_images(std::function) const; virtual bool for_blocks_range(const uint64_t& h1, const uint64_t& h2, std::function) const; virtual bool for_all_transactions(std::function) const; - virtual bool for_all_outputs(std::function f) const; + virtual bool for_all_outputs(std::function f) const; + virtual bool for_all_outputs(uint64_t amount, const std::function &f) const; virtual uint64_t add_block( const block& blk , const size_t& block_size diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index a0031559b..0a6f1cdcd 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -1903,6 +1903,45 @@ void Blockchain::get_output_key_mask_unlocked(const uint64_t& amount, const uint unlocked = is_tx_spendtime_unlocked(m_db->get_tx_unlock_time(toi.first)); } //------------------------------------------------------------------ +bool Blockchain::get_output_distribution(uint64_t amount, uint64_t from_height, uint64_t &start_height, std::vector &distribution, uint64_t &base) const +{ + // rct outputs don't exist before v3 + if (amount == 0) + { + switch (m_nettype) + { + case STAGENET: start_height = stagenet_hard_forks[2].height; break; + case TESTNET: start_height = testnet_hard_forks[2].height; break; + case MAINNET: start_height = mainnet_hard_forks[2].height; break; + default: return false; + } + } + else + start_height = 0; + base = 0; + + const uint64_t real_start_height = start_height; + if (from_height > start_height) + start_height = from_height; + + distribution.clear(); + uint64_t db_height = m_db->height(); + if (start_height >= db_height) + return false; + distribution.resize(db_height - start_height, 0); + bool r = for_all_outputs(amount, [&](uint64_t height) { + CHECK_AND_ASSERT_MES(height >= real_start_height && height <= db_height, false, "Height not in expected range"); + if (height >= start_height) + distribution[height - start_height]++; + else + base++; + return true; + }); + if (!r) + return false; + return true; +} +//------------------------------------------------------------------ // This function takes a list of block hashes from another node // on the network to find where the split point is between us and them. // This is used to see what to send another node that needs to sync. @@ -4441,11 +4480,16 @@ bool Blockchain::for_all_transactions(std::functionfor_all_transactions(f); } -bool Blockchain::for_all_outputs(std::function f) const +bool Blockchain::for_all_outputs(std::function f) const { return m_db->for_all_outputs(f);; } +bool Blockchain::for_all_outputs(uint64_t amount, std::function f) const +{ + return m_db->for_all_outputs(amount, f);; +} + namespace cryptonote { template bool Blockchain::get_transactions(const std::vector&, std::list&, std::list&) const; } diff --git a/src/cryptonote_core/blockchain.h b/src/cryptonote_core/blockchain.h index 1a52e20bf..4423199de 100644 --- a/src/cryptonote_core/blockchain.h +++ b/src/cryptonote_core/blockchain.h @@ -523,6 +523,17 @@ namespace cryptonote */ bool get_random_rct_outs(const COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS::request& req, COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS::response& res) const; + /** + * @brief gets per block distribution of outputs of a given amount + * + * @param amount the amount to get a ditribution for + * @param return-by-reference from_height the height before which we do not care about the data + * @param return-by-reference start_height the height of the first rct output + * @param return-by-reference distribution the start offset of the first rct output in this block (same as previous if none) + * @param return-by-reference base how many outputs of that amount are before the stated distribution + */ + bool get_output_distribution(uint64_t amount, uint64_t from_height, uint64_t &start_height, std::vector &distribution, uint64_t &base) const; + /** * @brief gets the global indices for outputs from a given transaction * @@ -857,7 +868,17 @@ namespace cryptonote * * @return false if any output fails the check, otherwise true */ - bool for_all_outputs(std::function) const; + bool for_all_outputs(std::function) const; + + /** + * @brief perform a check on all outputs of a given amount in the blockchain + * + * @param amount the amount to iterate through + * @param std::function the check to perform, pass/fail + * + * @return false if any output fails the check, otherwise true + */ + bool for_all_outputs(uint64_t amount, std::function) const; /** * @brief get a reference to the BlockchainDB in use by Blockchain diff --git a/src/cryptonote_core/cryptonote_core.cpp b/src/cryptonote_core/cryptonote_core.cpp index 8c0118803..a91cc9638 100644 --- a/src/cryptonote_core/cryptonote_core.cpp +++ b/src/cryptonote_core/cryptonote_core.cpp @@ -1105,6 +1105,11 @@ namespace cryptonote return m_blockchain_storage.get_random_rct_outs(req, res); } //----------------------------------------------------------------------------------------------- + bool core::get_output_distribution(uint64_t amount, uint64_t from_height, uint64_t &start_height, std::vector &distribution, uint64_t &base) const + { + return m_blockchain_storage.get_output_distribution(amount, from_height, start_height, distribution, base); + } + //----------------------------------------------------------------------------------------------- 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); diff --git a/src/cryptonote_core/cryptonote_core.h b/src/cryptonote_core/cryptonote_core.h index e1e430516..abf79be1d 100644 --- a/src/cryptonote_core/cryptonote_core.h +++ b/src/cryptonote_core/cryptonote_core.h @@ -571,6 +571,12 @@ namespace cryptonote */ bool get_random_rct_outs(const COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS::request& req, COMMAND_RPC_GET_RANDOM_RCT_OUTPUTS::response& res) const; + /** + * @copydoc Blockchain::get_output_distribution + * + * @brief get per block distribution of outputs of a given amount + */ + bool get_output_distribution(uint64_t amount, uint64_t from_height, uint64_t &start_height, std::vector &distribution, uint64_t &base) const; /** * @copydoc miner::pause diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index 0e3e6757b..f649a0ca5 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -2076,6 +2076,41 @@ namespace cryptonote return true; } //------------------------------------------------------------------------------------------------------------------------------ + bool core_rpc_server::on_get_output_distribution(const COMMAND_RPC_GET_OUTPUT_DISTRIBUTION::request& req, COMMAND_RPC_GET_OUTPUT_DISTRIBUTION::response& res, epee::json_rpc::error& error_resp) + { + try + { + for (uint64_t amount: req.amounts) + { + std::vector distribution; + uint64_t start_height, base; + if (!m_core.get_output_distribution(amount, req.from_height, start_height, distribution, base)) + { + error_resp.code = CORE_RPC_ERROR_CODE_INTERNAL_ERROR; + error_resp.message = "Failed to get rct distribution"; + return false; + } + if (req.cumulative) + { + distribution[0] += base; + for (size_t n = 1; n < distribution.size(); ++n) + distribution[n] += distribution[n-1]; + } + res.distributions.push_back({amount, start_height, std::move(distribution), base}); + } + } + catch (const std::exception &e) + { + error_resp.code = CORE_RPC_ERROR_CODE_INTERNAL_ERROR; + error_resp.message = "Failed to get output distribution"; + return false; + } + + res.status = CORE_RPC_STATUS_OK; + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ + const command_line::arg_descriptor core_rpc_server::arg_rpc_bind_port = { "rpc-bind-port" diff --git a/src/rpc/core_rpc_server.h b/src/rpc/core_rpc_server.h index 4754a5d5f..a5755e062 100644 --- a/src/rpc/core_rpc_server.h +++ b/src/rpc/core_rpc_server.h @@ -153,6 +153,7 @@ namespace cryptonote MAP_JON_RPC_WE_IF("relay_tx", on_relay_tx, COMMAND_RPC_RELAY_TX, !m_restricted) MAP_JON_RPC_WE_IF("sync_info", on_sync_info, COMMAND_RPC_SYNC_INFO, !m_restricted) MAP_JON_RPC_WE("get_txpool_backlog", on_get_txpool_backlog, COMMAND_RPC_GET_TRANSACTION_POOL_BACKLOG) + MAP_JON_RPC_WE_IF("get_output_distribution", on_get_output_distribution, COMMAND_RPC_GET_OUTPUT_DISTRIBUTION, !m_restricted) END_JSON_RPC_MAP() END_URI_MAP2() @@ -214,6 +215,7 @@ namespace cryptonote bool on_relay_tx(const COMMAND_RPC_RELAY_TX::request& req, COMMAND_RPC_RELAY_TX::response& res, epee::json_rpc::error& error_resp); bool on_sync_info(const COMMAND_RPC_SYNC_INFO::request& req, COMMAND_RPC_SYNC_INFO::response& res, epee::json_rpc::error& error_resp); bool on_get_txpool_backlog(const COMMAND_RPC_GET_TRANSACTION_POOL_BACKLOG::request& req, COMMAND_RPC_GET_TRANSACTION_POOL_BACKLOG::response& res, epee::json_rpc::error& error_resp); + bool on_get_output_distribution(const COMMAND_RPC_GET_OUTPUT_DISTRIBUTION::request& req, COMMAND_RPC_GET_OUTPUT_DISTRIBUTION::response& res, epee::json_rpc::error& error_resp); //----------------------- private: diff --git a/src/rpc/core_rpc_server_commands_defs.h b/src/rpc/core_rpc_server_commands_defs.h index a2c780376..660fb7889 100644 --- a/src/rpc/core_rpc_server_commands_defs.h +++ b/src/rpc/core_rpc_server_commands_defs.h @@ -49,7 +49,7 @@ namespace cryptonote // advance which version they will stop working with // Don't go over 32767 for any of these #define CORE_RPC_VERSION_MAJOR 1 -#define CORE_RPC_VERSION_MINOR 18 +#define CORE_RPC_VERSION_MINOR 19 #define MAKE_CORE_RPC_VERSION(major,minor) (((major)<<16)|(minor)) #define CORE_RPC_VERSION MAKE_CORE_RPC_VERSION(CORE_RPC_VERSION_MAJOR, CORE_RPC_VERSION_MINOR) @@ -2203,4 +2203,47 @@ namespace cryptonote END_KV_SERIALIZE_MAP() }; }; + + struct COMMAND_RPC_GET_OUTPUT_DISTRIBUTION + { + struct request + { + std::vector amounts; + uint64_t from_height; + bool cumulative; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(amounts) + KV_SERIALIZE_OPT(from_height, (uint64_t)0) + KV_SERIALIZE_OPT(cumulative, false) + END_KV_SERIALIZE_MAP() + }; + + struct distribution + { + uint64_t amount; + uint64_t start_height; + std::vector distribution; + uint64_t base; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(amount) + KV_SERIALIZE(start_height) + KV_SERIALIZE_CONTAINER_POD_AS_BLOB(distribution) + KV_SERIALIZE(base) + END_KV_SERIALIZE_MAP() + }; + + struct response + { + std::string status; + std::vector distributions; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(status) + KV_SERIALIZE(distributions) + END_KV_SERIALIZE_MAP() + }; + }; + } diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index cb5e7bc67..dea584471 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -2266,6 +2266,71 @@ bool wallet2::refresh(uint64_t & blocks_fetched, bool& received_money, bool& ok) return ok; } //---------------------------------------------------------------------------------------------------- +bool wallet2::get_output_distribution(uint64_t &start_height, std::vector &distribution) +{ + uint32_t rpc_version; + boost::optional result = m_node_rpc_proxy.get_rpc_version(rpc_version); + // no error + if (!!result) + { + // empty string -> not connection + THROW_WALLET_EXCEPTION_IF(result->empty(), tools::error::no_connection_to_daemon, "getversion"); + THROW_WALLET_EXCEPTION_IF(*result == CORE_RPC_STATUS_BUSY, tools::error::daemon_busy, "getversion"); + if (*result != CORE_RPC_STATUS_OK) + { + MDEBUG("Cannot determine daemon RPC version, not requesting rct distribution"); + return false; + } + } + else + { + if (rpc_version >= MAKE_CORE_RPC_VERSION(1, 19)) + { + MDEBUG("Daemon is recent enough, requesting rct distribution"); + } + else + { + MDEBUG("Daemon is too old, not requesting rct distribution"); + return false; + } + } + + cryptonote::COMMAND_RPC_GET_OUTPUT_DISTRIBUTION::request req = AUTO_VAL_INIT(req); + cryptonote::COMMAND_RPC_GET_OUTPUT_DISTRIBUTION::response res = AUTO_VAL_INIT(res); + req.amounts.push_back(0); + m_daemon_rpc_mutex.lock(); + bool r = net_utils::invoke_http_json_rpc("/json_rpc", "get_output_distribution", req, res, m_http_client, rpc_timeout); + m_daemon_rpc_mutex.unlock(); + if (!r) + { + MWARNING("Failed to request output distribution: no connection to daemon"); + return false; + } + if (res.status == CORE_RPC_STATUS_BUSY) + { + MWARNING("Failed to request output distribution: daemon is busy"); + return false; + } + if (res.status != CORE_RPC_STATUS_OK) + { + MWARNING("Failed to request output distribution: " << res.status); + return false; + } + if (res.distributions.size() != 1) + { + MWARNING("Failed to request output distribution: not the expected single result"); + return false; + } + if (res.distributions[0].amount != 0) + { + MWARNING("Failed to request output distribution: results are not for amount 0"); + return false; + } + start_height = res.distributions[0].start_height; + distribution = std::move(res.distributions[0].distribution); + return true; +} +//---------------------------------------------------------------------------------------------------- void wallet2::detach_blockchain(uint64_t height) { LOG_PRINT_L0("Detaching blockchain on height " << height); diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index d2e484e05..2a883dce7 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -1103,6 +1103,8 @@ namespace tools rct::key get_multisig_k(size_t idx, const std::unordered_set &used_L) const; void update_multisig_rescan_info(const std::vector> &multisig_k, const std::vector> &info, size_t n); + bool get_output_distribution(uint64_t &start_height, std::vector &distribution); + cryptonote::account_base m_account; boost::optional m_daemon_login; std::string m_daemon_address; diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h index 32a0231b1..7611d3d31 100644 --- a/src/wallet/wallet_errors.h +++ b/src/wallet/wallet_errors.h @@ -85,6 +85,7 @@ namespace tools // no_connection_to_daemon // is_key_image_spent_error // get_histogram_error + // get_output_distribution // wallet_files_doesnt_correspond // // * - class with protected ctor @@ -757,6 +758,14 @@ namespace tools } }; //---------------------------------------------------------------------------------------------------- + struct get_output_distribution : public wallet_rpc_error + { + explicit get_output_distribution(std::string&& loc, const std::string& request) + : wallet_rpc_error(std::move(loc), "failed to get output distribution", request) + { + } + }; + //---------------------------------------------------------------------------------------------------- struct wallet_files_doesnt_correspond : public wallet_logic_error { explicit wallet_files_doesnt_correspond(std::string&& loc, const std::string& keys_file, const std::string& wallet_file) diff --git a/tests/unit_tests/hardfork.cpp b/tests/unit_tests/hardfork.cpp index a2038ff0c..3e2199217 100644 --- a/tests/unit_tests/hardfork.cpp +++ b/tests/unit_tests/hardfork.cpp @@ -109,7 +109,8 @@ public: virtual bool for_all_key_images(std::function) const { return true; } virtual bool for_blocks_range(const uint64_t&, const uint64_t&, std::function) const { return true; } virtual bool for_all_transactions(std::function) const { return true; } - virtual bool for_all_outputs(std::function f) const { return true; } + virtual bool for_all_outputs(std::function f) const { return true; } + virtual bool for_all_outputs(uint64_t amount, const std::function &f) const { return true; } virtual bool is_read_only() const { return false; } virtual std::map> get_output_histogram(const std::vector &amounts, bool unlocked, uint64_t recent_cutoff) const { return std::map>(); }