diff options
25 files changed, 765 insertions, 129 deletions
diff --git a/src/blockchain_db/berkeleydb/db_bdb.cpp b/src/blockchain_db/berkeleydb/db_bdb.cpp index 1fef9e619..a7fa556bd 100644 --- a/src/blockchain_db/berkeleydb/db_bdb.cpp +++ b/src/blockchain_db/berkeleydb/db_bdb.cpp @@ -2180,6 +2180,12 @@ void BlockchainBDB::get_output_tx_and_index(const uint64_t& amount, const std::v LOG_PRINT_L3("db3: " << db3); } +std::map<uint64_t, uint64_t>::BlockchainBDB::get_output_histogram(const std::vector<uint64_t> &amounts) const +{ + LOG_PRINT_L3("BlockchainBDB::" << __func__); + throw1(DB_ERROR("Not implemented.")); +} + void BlockchainBDB::set_hard_fork_starting_height(uint8_t version, uint64_t height) { LOG_PRINT_L3("BlockchainBDB::" << __func__); diff --git a/src/blockchain_db/berkeleydb/db_bdb.h b/src/blockchain_db/berkeleydb/db_bdb.h index d7cbd24e7..5c6bda4eb 100644 --- a/src/blockchain_db/berkeleydb/db_bdb.h +++ b/src/blockchain_db/berkeleydb/db_bdb.h @@ -341,6 +341,15 @@ public: virtual bool can_thread_bulk_indices() const { return false; } #endif + /** + * @brief return a histogram of outputs on the blockchain + * + * @param amounts optional set of amounts to lookup + * + * @return a set of amount/instances + */ + std::map<uint64_t, uint64_t> get_output_histogram(const std::vector<uint64_t> &amounts) const; + private: virtual void add_block( const block& blk , const size_t& block_size diff --git a/src/blockchain_db/blockchain_db.h b/src/blockchain_db/blockchain_db.h index 3e0ca141e..3585bd061 100644 --- a/src/blockchain_db/blockchain_db.h +++ b/src/blockchain_db/blockchain_db.h @@ -1289,6 +1289,15 @@ public: virtual void drop_hard_fork_info() = 0; /** + * @brief return a histogram of outputs on the blockchain + * + * @param amounts optional set of amounts to lookup + * + * @return a set of amount/instances + */ + virtual std::map<uint64_t, uint64_t> get_output_histogram(const std::vector<uint64_t> &amounts) const = 0; + + /** * @brief is BlockchainDB in read-only mode? * * @return true if in read-only mode, otherwise false diff --git a/src/blockchain_db/lmdb/db_lmdb.cpp b/src/blockchain_db/lmdb/db_lmdb.cpp index e928ab803..9b99520a1 100644 --- a/src/blockchain_db/lmdb/db_lmdb.cpp +++ b/src/blockchain_db/lmdb/db_lmdb.cpp @@ -2692,6 +2692,63 @@ void BlockchainLMDB::get_output_tx_and_index(const uint64_t& amount, const std:: LOG_PRINT_L3("db3: " << db3); } +std::map<uint64_t, uint64_t> BlockchainLMDB::get_output_histogram(const std::vector<uint64_t> &amounts) const +{ + LOG_PRINT_L3("BlockchainLMDB::" << __func__); + check_open(); + + TXN_PREFIX_RDONLY(); + RCURSOR(output_amounts); + + std::map<uint64_t, uint64_t> histogram; + MDB_val k; + MDB_val v; + + if (amounts.empty()) + { + MDB_cursor_op op = MDB_FIRST; + while (1) + { + int ret = mdb_cursor_get(m_cur_output_amounts, &k, &v, op); + op = MDB_NEXT_NODUP; + if (ret == MDB_NOTFOUND) + break; + if (ret) + throw0(DB_ERROR(lmdb_error("Failed to enumerate outputs: ", ret).c_str())); + mdb_size_t num_elems = 0; + mdb_cursor_count(m_cur_output_amounts, &num_elems); + uint64_t amount = *(const uint64_t*)k.mv_data; + histogram[amount] = num_elems; + } + } + else + { + for (const auto &amount: amounts) + { + MDB_val_copy<uint64_t> k(amount); + int ret = mdb_cursor_get(m_cur_output_amounts, &k, &v, MDB_SET); + if (ret == MDB_NOTFOUND) + { + histogram[amount] = 0; + } + else if (ret == MDB_SUCCESS) + { + mdb_size_t num_elems = 0; + mdb_cursor_count(m_cur_output_amounts, &num_elems); + histogram[amount] = num_elems; + } + else + { + throw0(DB_ERROR(lmdb_error("Failed to enumerate outputs: ", ret).c_str())); + } + } + } + + TXN_POSTFIX_RDONLY(); + + return histogram; +} + void BlockchainLMDB::check_hard_fork_info() { LOG_PRINT_L3("BlockchainLMDB::" << __func__); diff --git a/src/blockchain_db/lmdb/db_lmdb.h b/src/blockchain_db/lmdb/db_lmdb.h index a3f32ffaa..6cd3e0e8f 100644 --- a/src/blockchain_db/lmdb/db_lmdb.h +++ b/src/blockchain_db/lmdb/db_lmdb.h @@ -280,6 +280,16 @@ public: virtual void pop_block(block& blk, std::vector<transaction>& txs); virtual bool can_thread_bulk_indices() const { return true; } + + /** + * @brief return a histogram of outputs on the blockchain + * + * @param amounts optional set of amounts to lookup + * + * @return a set of amount/instances + */ + std::map<uint64_t, uint64_t> get_output_histogram(const std::vector<uint64_t> &amounts) const; + private: void do_resize(uint64_t size_increase=0); diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 0f6afe740..da14d7575 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -3322,6 +3322,11 @@ bool Blockchain::get_hard_fork_voting_info(uint8_t version, uint32_t &window, ui return m_hardfork->get_voting_info(version, window, votes, threshold, earliest_height, voting); } +std::map<uint64_t, uint64_t> Blockchain:: get_output_histogram(const std::vector<uint64_t> &amounts) const +{ + return m_db->get_output_histogram(amounts); +} + void Blockchain::load_compiled_in_block_hashes() { if (m_fast_sync && get_blocks_dat_start(m_testnet) != nullptr) diff --git a/src/cryptonote_core/blockchain.h b/src/cryptonote_core/blockchain.h index 393faef2a..6bae0364d 100644 --- a/src/cryptonote_core/blockchain.h +++ b/src/cryptonote_core/blockchain.h @@ -683,6 +683,15 @@ namespace cryptonote bool flush_txes_from_pool(const std::list<crypto::hash> &txids); /** + * @brief return a histogram of outputs on the blockchain + * + * @param amounts optional set of amounts to lookup + * + * @return a set of amount/instances + */ + std::map<uint64_t, uint64_t> get_output_histogram(const std::vector<uint64_t> &amounts) const; + + /** * @brief perform a check on all key images in the blockchain * * @param std::function the check to perform, pass/fail diff --git a/src/cryptonote_core/tx_pool.cpp b/src/cryptonote_core/tx_pool.cpp index 5d67acdd2..49fa4aa57 100644 --- a/src/cryptonote_core/tx_pool.cpp +++ b/src/cryptonote_core/tx_pool.cpp @@ -54,6 +54,11 @@ namespace cryptonote { namespace { + //TODO: constants such as these should at least be in the header, + // but probably somewhere more accessible to the rest of the + // codebase. As it stands, it is at best nontrivial to test + // whether or not changing these parameters (or adding new) + // will work correctly. size_t const TRANSACTION_SIZE_LIMIT_V1 = (((CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V1 * 125) / 100) - CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE); size_t const TRANSACTION_SIZE_LIMIT_V2 = (((CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V2 * 125) / 100) - CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE); time_t const MIN_RELAY_TIME = (60 * 5); // only start re-relaying transactions after that many seconds @@ -116,11 +121,12 @@ namespace cryptonote return false; } + // fee per kilobyte, size rounded up. uint64_t fee = inputs_amount - outputs_amount; uint64_t needed_fee = blob_size / 1024; needed_fee += (blob_size % 1024) ? 1 : 0; needed_fee *= FEE_PER_KB; - if (!kept_by_block && fee < needed_fee /*&& fee < MINING_ALLOWED_LEGACY_FEE*/) + if (!kept_by_block && fee < needed_fee) { LOG_PRINT_L1("transaction fee is not enough: " << print_money(fee) << ", minimum fee: " << print_money(needed_fee)); tvc.m_verifivation_failed = true; @@ -135,7 +141,9 @@ namespace cryptonote return false; } - //check key images for transaction if it is not kept by block + // if the transaction came from a block popped from the chain, + // don't check if we have its key images as spent. + // TODO: Investigate why not? if(!kept_by_block) { if(have_tx_keyimges_as_spent(tx)) @@ -163,9 +171,10 @@ namespace cryptonote CRITICAL_REGION_LOCAL(m_transactions_lock); if(!ch_inp_res) { + // if the transaction was valid before (kept_by_block), then it + // may become valid again, so ignore the failed inputs check. if(kept_by_block) { - //anyway add this transaction to pool, because it related to block auto txd_p = m_transactions.insert(transactions_container::value_type(id, tx_details())); CHECK_AND_ASSERT_MES(txd_p.second, false, "transaction already exists at inserting in memory pool"); txd_p.first->second.blob_size = blob_size; @@ -207,13 +216,14 @@ namespace cryptonote tvc.m_should_be_relayed = true; } + // assume failure during verification steps until success is certain tvc.m_verifivation_failed = true; - //update image_keys container, here should everything goes ok. + BOOST_FOREACH(const auto& in, tx.vin) { CHECKED_GET_SPECIFIC_VARIANT(in, const txin_to_key, txin, false); std::unordered_set<crypto::hash>& kei_image_set = m_spent_key_images[txin.k_image]; - CHECK_AND_ASSERT_MES(kept_by_block || kei_image_set.size() == 0, false, "internal error: keeped_by_block=" << kept_by_block + CHECK_AND_ASSERT_MES(kept_by_block || kei_image_set.size() == 0, false, "internal error: kept_by_block=" << kept_by_block << ", kei_image_set.size()=" << kei_image_set.size() << ENDL << "txin.k_image=" << txin.k_image << ENDL << "tx_id=" << id ); auto ins_res = kei_image_set.insert(id); @@ -223,7 +233,7 @@ namespace cryptonote tvc.m_verifivation_failed = false; m_txs_by_fee.emplace((double)blob_size / fee, id); - //succeed + return true; } //--------------------------------------------------------------------------------- @@ -235,6 +245,9 @@ namespace cryptonote return add_tx(tx, h, blob_size, tvc, keeped_by_block, relayed, version); } //--------------------------------------------------------------------------------- + //FIXME: Can return early before removal of all of the key images. + // At the least, need to make sure that a false return here + // is treated properly. Should probably not return early, however. bool tx_memory_pool::remove_transaction_keyimages(const transaction& tx) { CRITICAL_REGION_LOCAL(m_transactions_lock); @@ -301,7 +314,7 @@ namespace cryptonote ); } //--------------------------------------------------------------------------------- - //proper tx_pool handling courtesy of CryptoZoidberg and Boolberry + //TODO: investigate whether boolean return is appropriate bool tx_memory_pool::remove_stuck_transactions() { CRITICAL_REGION_LOCAL(m_transactions_lock); @@ -332,6 +345,7 @@ namespace cryptonote return true; } //--------------------------------------------------------------------------------- + //TODO: investigate whether boolean return is appropriate bool tx_memory_pool::get_relayable_transactions(std::list<std::pair<crypto::hash, cryptonote::transaction>> &txs) const { CRITICAL_REGION_LOCAL(m_transactions_lock); @@ -380,6 +394,7 @@ namespace cryptonote txs.push_back(tx_vt.second.tx); } //------------------------------------------------------------------ + //TODO: investigate whether boolean return is appropriate bool tx_memory_pool::get_transactions_and_spent_keys_info(std::vector<tx_info>& tx_infos, std::vector<spent_key_image_info>& key_image_infos) const { CRITICAL_REGION_LOCAL(m_transactions_lock); @@ -556,6 +571,7 @@ namespace cryptonote return ss.str(); } //--------------------------------------------------------------------------------- + //TODO: investigate whether boolean return is appropriate bool tx_memory_pool::fill_block_template(block &bl, size_t median_size, uint64_t already_generated_coins, size_t &total_size, uint64_t &fee) { // Warning: This function takes already_generated_ @@ -646,6 +662,7 @@ namespace cryptonote return n_removed; } //--------------------------------------------------------------------------------- + //TODO: investigate whether only ever returning true is correct bool tx_memory_pool::init(const std::string& config_folder) { CRITICAL_REGION_LOCAL(m_transactions_lock); @@ -679,6 +696,7 @@ namespace cryptonote } //--------------------------------------------------------------------------------- + //TODO: investigate whether only ever returning true is correct bool tx_memory_pool::deinit() { if (m_config_folder.empty()) diff --git a/src/cryptonote_core/tx_pool.h b/src/cryptonote_core/tx_pool.h index 71febcab6..84e11eeff 100644 --- a/src/cryptonote_core/tx_pool.h +++ b/src/cryptonote_core/tx_pool.h @@ -57,7 +57,9 @@ namespace cryptonote /* */ /************************************************************************/ + //! pair of <transaction fee, transaction hash> for organization typedef std::pair<double, crypto::hash> tx_by_fee_entry; + class txCompare { public: @@ -71,47 +73,255 @@ namespace cryptonote } }; + //! container for sorting transactions by fee per unit size typedef std::set<tx_by_fee_entry, txCompare> sorted_tx_container; + /** + * @brief Transaction pool, handles transactions which are not part of a block + * + * This class handles all transactions which have been received, but not as + * part of a block. + * + * This handling includes: + * storing the transactions + * organizing the transactions by fee per size + * taking/giving transactions to and from various other components + * saving the transactions to disk on shutdown + * helping create a new block template by choosing transactions for it + * + */ class tx_memory_pool: boost::noncopyable { public: #if BLOCKCHAIN_DB == DB_LMDB + /** + * @brief Constructor + * + * @param bchs a Blockchain class instance, for getting chain info + */ tx_memory_pool(Blockchain& bchs); #else tx_memory_pool(blockchain_storage& bchs); #endif - bool add_tx(const transaction &tx, const crypto::hash &id, size_t blob_size, tx_verification_context& tvc, bool keeped_by_block, bool relayed, uint8_t version); - bool add_tx(const transaction &tx, tx_verification_context& tvc, bool keeped_by_block, bool relayed, uint8_t version); - //gets tx and remove it from pool + + + /** + * @copydoc add_tx(const transaction&, tx_verification_context&, bool, bool, uint8_t) + * + * @param id the transaction's hash + * @param blob_size the transaction's size + */ + bool add_tx(const transaction &tx, const crypto::hash &id, size_t blob_size, tx_verification_context& tvc, bool kept_by_block, bool relayed, uint8_t version); + + /** + * @brief add a transaction to the transaction pool + * + * Most likely the transaction will come from the network, but it is + * also possible for transactions to come from popped blocks during + * a reorg, or from local clients creating a transaction and + * submitting it to the network + * + * @param tx the transaction to be added + * @param tvc return-by-reference status about the transaction verification + * @param kept_by_block has this transaction been in a block? + * @param relayed was this transaction from the network or a local client? + * @param version the version used to create the transaction + * + * @return true if the transaction passes validations, otherwise false + */ + bool add_tx(const transaction &tx, tx_verification_context& tvc, bool kept_by_block, bool relayed, uint8_t version); + + /** + * @brief takes a transaction with the given hash from the pool + * + * @param id the hash of the transaction + * @param tx return-by-reference the transaction taken + * @param blob_size return-by-reference the transaction's size + * @param fee the transaction fee + * @param relayed return-by-reference was transaction relayed to us by the network? + * + * @return true unless the transaction cannot be found in the pool + */ bool take_tx(const crypto::hash &id, transaction &tx, size_t& blob_size, uint64_t& fee, bool &relayed); + /** + * @brief checks if the pool has a transaction with the given hash + * + * @param id the hash to look for + * + * @return true if the transaction is in the pool, otherwise false + */ bool have_tx(const crypto::hash &id) const; + + /** + * @brief action to take when notified of a block added to the blockchain + * + * Currently does nothing + * + * @param new_block_height the height of the blockchain after the change + * @param top_block_id the hash of the new top block + * + * @return true + */ bool on_blockchain_inc(uint64_t new_block_height, const crypto::hash& top_block_id); + + /** + * @brief action to take when notified of a block removed from the blockchain + * + * Currently does nothing + * + * @param new_block_height the height of the blockchain after the change + * @param top_block_id the hash of the new top block + * + * @return true + */ bool on_blockchain_dec(uint64_t new_block_height, const crypto::hash& top_block_id); + + /** + * @brief action to take periodically + * + * Currently checks transaction pool for stale ("stuck") transactions + */ void on_idle(); + /** + * @brief locks the transaction pool + */ void lock() const; + + /** + * @brief unlocks the transaction pool + */ void unlock() const; // load/store operations + + /** + * @brief loads pool state (if any) from disk, and initializes pool + * + * @param config_folder folder name where pool state will be + * + * @return true + */ bool init(const std::string& config_folder); + + /** + * @brief attempts to save the transaction pool state to disk + * + * Currently fails (returns false) if the data directory from init() + * does not exist and cannot be created, but returns true even if + * saving to disk is unsuccessful. + * + * @return true in most cases (see above) + */ bool deinit(); + + /** + * @brief Chooses transactions for a block to include + * + * @param bl return-by-reference the block to fill in with transactions + * @param median_size the current median block size + * @param already_generated_coins the current total number of coins "minted" + * @param total_size return-by-reference the total size of the new block + * @param fee return-by-reference the total of fees from the included transactions + * + * @return true + */ bool fill_block_template(block &bl, size_t median_size, uint64_t already_generated_coins, size_t &total_size, uint64_t &fee); + + /** + * @brief get a list of all transactions in the pool + * + * @param txs return-by-reference the list of transactions + */ void get_transactions(std::list<transaction>& txs) const; + + /** + * @brief get information about all transactions and key images in the pool + * + * see documentation on tx_info and spent_key_image_info for more details + * + * @param tx_infos return-by-reference the transactions' information + * @param key_image_infos return-by-reference the spent key images' information + * + * @return true + */ bool get_transactions_and_spent_keys_info(std::vector<tx_info>& tx_infos, std::vector<spent_key_image_info>& key_image_infos) const; + + /** + * @brief get a specific transaction from the pool + * + * @param h the hash of the transaction to get + * @param tx return-by-reference the transaction requested + * + * @return true if the transaction is found, otherwise false + */ bool get_transaction(const crypto::hash& h, transaction& tx) const; + + /** + * @brief get a list of all relayable transactions and their hashes + * + * "relayable" in this case means: + * nonzero fee + * hasn't been relayed too recently + * isn't old enough that relaying it is considered harmful + * + * @param txs return-by-reference the transactions and their hashes + * + * @return true + */ bool get_relayable_transactions(std::list<std::pair<crypto::hash, cryptonote::transaction>>& txs) const; + + /** + * @brief tell the pool that certain transactions were just relayed + * + * @param txs the list of transactions (and their hashes) + */ void set_relayed(const std::list<std::pair<crypto::hash, cryptonote::transaction>>& txs); + + /** + * @brief get the total number of transactions in the pool + * + * @return the number of transactions in the pool + */ size_t get_transactions_count() const; + + /** + * @brief get a string containing human-readable pool information + * + * @param short_format whether to use a shortened format for the info + * + * @return the string + */ std::string print_pool(bool short_format) const; + + /** + * @brief remove transactions from the pool which are no longer valid + * + * With new versions of the currency, what conditions render a transaction + * invalid may change. This function clears those which were received + * before a version change and no longer conform to requirements. + * + * @param version the version the transactions must conform to + * + * @return the number of transactions removed + */ size_t validate(uint8_t version); - /*bool flush_pool(const std::strig& folder); - bool inflate_pool(const std::strig& folder);*/ #define CURRENT_MEMPOOL_ARCHIVE_VER 11 + /** + * @brief serialize the transaction pool to/from disk + * + * If the archive version passed is older than the version compiled + * in, this function does nothing, as it cannot deserialize after a + * format change. + * + * @tparam archive_t the archive class + * @param a the archive to serialize to/from + * @param version the archive version + */ template<class archive_t> void serialize(archive_t & a, const unsigned int version) { @@ -123,97 +333,169 @@ namespace cryptonote a & m_timed_out_transactions; } + /** + * @brief information about a single transaction + */ struct tx_details { - transaction tx; - size_t blob_size; - uint64_t fee; - crypto::hash max_used_block_id; - uint64_t max_used_block_height; - bool kept_by_block; - // - uint64_t last_failed_height; + transaction tx; //!< the transaction + size_t blob_size; //!< the transaction's size + uint64_t fee; //!< the transaction's fee amount + crypto::hash max_used_block_id; //!< the hash of the highest block referenced by an input + uint64_t max_used_block_height; //!< the height of the highest block referenced by an input + + //! whether or not the transaction has been in a block before + /*! if the transaction was returned to the pool from the blockchain + * due to a reorg, then this will be true + */ + bool kept_by_block; + + //! the highest block the transaction referenced when last checking it failed + /*! if verifying a transaction's inputs fails, it's possible this is due + * to a reorg since it was created (if it used recently created outputs + * as inputs). + */ + uint64_t last_failed_height; + + //! the hash of the highest block the transaction referenced when last checking it failed + /*! if verifying a transaction's inputs fails, it's possible this is due + * to a reorg since it was created (if it used recently created outputs + * as inputs). + */ crypto::hash last_failed_id; - time_t receive_time; - time_t last_relayed_time; - bool relayed; + time_t receive_time; //!< the time when the transaction entered the pool + + time_t last_relayed_time; //!< the last time the transaction was relayed to the network + bool relayed; //!< whether or not the transaction has been relayed to the network }; private: + + /** + * @brief remove old transactions from the pool + * + * After a certain time, it is assumed that a transaction which has not + * yet been mined will likely not be mined. These transactions are removed + * from the pool to avoid buildup. + * + * @return true + */ bool remove_stuck_transactions(); + + /** + * @brief check if a transaction in the pool has a given spent key image + * + * @param key_im the spent key image to look for + * + * @return true if the spent key image is present, otherwise false + */ bool have_tx_keyimg_as_spent(const crypto::key_image& key_im) const; + + /** + * @brief check if any spent key image in a transaction is in the pool + * + * Checks if any of the spent key images in a given transaction are present + * in any of the transactions in the transaction pool. + * + * @note see tx_pool::have_tx_keyimg_as_spent + * + * @param tx the transaction to check spent key images of + * + * @return true if any spent key images are present in the pool, otherwise false + */ bool have_tx_keyimges_as_spent(const transaction& tx) const; + + /** + * @brief forget a transaction's spent key images + * + * Spent key images are stored separately from transactions for + * convenience/speed, so this is part of the process of removing + * a transaction from the pool. + * + * @param tx the transaction + * + * @return false if any key images to be removed cannot be found, otherwise true + */ bool remove_transaction_keyimages(const transaction& tx); + + /** + * @brief check if any of a transaction's spent key images are present in a given set + * + * @param kic the set of key images to check against + * @param tx the transaction to check + * + * @return true if any key images present in the set, otherwise false + */ static bool have_key_images(const std::unordered_set<crypto::key_image>& kic, const transaction& tx); + + /** + * @brief append the key images from a transaction to the given set + * + * @param kic the set of key images to append to + * @param tx the transaction + * + * @return false if any append fails, otherwise true + */ static bool append_key_images(std::unordered_set<crypto::key_image>& kic, const transaction& tx); + /** + * @brief check if a transaction is a valid candidate for inclusion in a block + * + * @param txd the transaction to check (and info about it) + * + * @return true if the transaction is good to go, otherwise false + */ bool is_transaction_ready_to_go(tx_details& txd) const; + + //! map transactions (and related info) by their hashes typedef std::unordered_map<crypto::hash, tx_details > transactions_container; + + //TODO: confirm the below comments and investigate whether or not this + // is the desired behavior + //! map key images to transactions which spent them + /*! this seems odd, but it seems that multiple transactions can exist + * in the pool which both have the same spent key. This would happen + * in the event of a reorg where someone creates a new/different + * transaction on the assumption that the original will not be in a + * block again. + */ typedef std::unordered_map<crypto::key_image, std::unordered_set<crypto::hash> > key_images_container; - mutable epee::critical_section m_transactions_lock; - transactions_container m_transactions; - key_images_container m_spent_key_images; + mutable epee::critical_section m_transactions_lock; //!< lock for the pool + transactions_container m_transactions; //!< container for transactions in the pool + + //! container for spent key images from the transactions in the pool + key_images_container m_spent_key_images; + + //TODO: this time should be a named constant somewhere, not hard-coded + //! interval on which to check for stale/"stuck" transactions epee::math_helper::once_a_time_seconds<30> m_remove_stuck_tx_interval; - //TODO: add fee_per_kb element to type tx_details and replace this - //functionality by just making m_transactions a std::set - sorted_tx_container m_txs_by_fee; + //TODO: look into doing this better + sorted_tx_container m_txs_by_fee; //!< container for transactions organized by fee per size + /** + * @brief get an iterator to a transaction in the sorted container + * + * @param id the hash of the transaction to look for + * + * @return an iterator, possibly to the end of the container if not found + */ sorted_tx_container::iterator find_tx_in_sorted_container(const crypto::hash& id) const; + //! transactions which are unlikely to be included in blocks + /*! These transactions are kept in RAM in case they *are* included + * in a block eventually, but this container is not saved to disk. + */ std::unordered_set<crypto::hash> m_timed_out_transactions; - //transactions_container m_alternative_transactions; - - std::string m_config_folder; + std::string m_config_folder; //!< the folder to save state to #if BLOCKCHAIN_DB == DB_LMDB - Blockchain& m_blockchain; + Blockchain& m_blockchain; //!< reference to the Blockchain object #else blockchain_storage& m_blockchain; #endif - /************************************************************************/ - /* */ - /************************************************************************/ - /*class inputs_visitor: public boost::static_visitor<bool> - { - key_images_container& m_spent_keys; - public: - inputs_visitor(key_images_container& spent_keys): m_spent_keys(spent_keys) - {} - bool operator()(const txin_to_key& tx) const - { - auto pr = m_spent_keys.insert(tx.k_image); - CHECK_AND_ASSERT_MES(pr.second, false, "Tried to insert transaction with input seems already spent, input: " << epee::string_tools::pod_to_hex(tx.k_image)); - return true; - } - bool operator()(const txin_gen& tx) const - { - CHECK_AND_ASSERT_MES(false, false, "coinbase transaction in memory pool"); - return false; - } - bool operator()(const txin_to_script& tx) const {return false;} - bool operator()(const txin_to_scripthash& tx) const {return false;} - }; */ - /************************************************************************/ - /* */ - /************************************************************************/ - class amount_visitor: public boost::static_visitor<uint64_t> - { - public: - uint64_t operator()(const txin_to_key& tx) const - { - return tx.amount; - } - uint64_t operator()(const txin_gen& tx) const - { - CHECK_AND_ASSERT_MES(false, false, "coinbase transaction in memory pool"); - return 0; - } - uint64_t operator()(const txin_to_script& tx) const {return 0;} - uint64_t operator()(const txin_to_scripthash& tx) const {return 0;} - }; #if BLOCKCHAIN_DB == DB_LMDB #else diff --git a/src/daemon/command_parser_executor.cpp b/src/daemon/command_parser_executor.cpp index 0b42257e7..166fe04ca 100644 --- a/src/daemon/command_parser_executor.cpp +++ b/src/daemon/command_parser_executor.cpp @@ -439,5 +439,23 @@ bool t_command_parser_executor::flush_txpool(const std::vector<std::string>& arg return m_executor.flush_txpool(txid); } +bool t_command_parser_executor::output_histogram(const std::vector<std::string>& args) +{ + if (args.size() > 2) return false; + + uint64_t min_count = 3; + uint64_t max_count = 0; + + if (args.size() >= 1) + { + min_count = boost::lexical_cast<uint64_t>(args[0]); + } + if (args.size() >= 2) + { + max_count = boost::lexical_cast<uint64_t>(args[1]); + } + return m_executor.output_histogram(min_count, max_count); +} + } // namespace daemonize diff --git a/src/daemon/command_parser_executor.h b/src/daemon/command_parser_executor.h index 51c55a8c0..11df92a5e 100644 --- a/src/daemon/command_parser_executor.h +++ b/src/daemon/command_parser_executor.h @@ -114,6 +114,8 @@ public: bool unban(const std::vector<std::string>& args); bool flush_txpool(const std::vector<std::string>& args); + + bool output_histogram(const std::vector<std::string>& args); }; } // namespace daemonize diff --git a/src/daemon/command_server.cpp b/src/daemon/command_server.cpp index 206de9d4a..aabc2f09a 100644 --- a/src/daemon/command_server.cpp +++ b/src/daemon/command_server.cpp @@ -214,6 +214,11 @@ t_command_server::t_command_server( , std::bind(&t_command_parser_executor::flush_txpool, &m_parser, p::_1) , "Flush a transaction from the tx pool by its txid, or the whole tx pool" ); + m_command_lookup.set_handler( + "output_histogram" + , std::bind(&t_command_parser_executor::output_histogram, &m_parser, p::_1) + , "Print output histogram (amount, instances)" + ); } bool t_command_server::process_command_str(const std::string& cmd) diff --git a/src/daemon/rpc_command_executor.cpp b/src/daemon/rpc_command_executor.cpp index 15ddc081f..933c93ed7 100644 --- a/src/daemon/rpc_command_executor.cpp +++ b/src/daemon/rpc_command_executor.cpp @@ -1203,5 +1203,41 @@ bool t_rpc_command_executor::flush_txpool(const std::string &txid) return true; } +bool t_rpc_command_executor::output_histogram(uint64_t min_count, uint64_t max_count) +{ + cryptonote::COMMAND_RPC_GET_OUTPUT_HISTOGRAM::request req; + cryptonote::COMMAND_RPC_GET_OUTPUT_HISTOGRAM::response res; + std::string fail_message = "Unsuccessful"; + epee::json_rpc::error error_resp; + + req.min_count = min_count; + req.max_count = max_count; + + if (m_is_rpc) + { + if (!m_rpc_client->json_rpc_request(req, res, "get_output_histogram", fail_message.c_str())) + { + return true; + } + } + else + { + if (!m_rpc_server->on_get_output_histogram(req, res, error_resp)) + { + tools::fail_msg_writer() << fail_message.c_str(); + return true; + } + } + + std::sort(res.histogram.begin(), res.histogram.end(), + [](const cryptonote::COMMAND_RPC_GET_OUTPUT_HISTOGRAM::entry &e1, const cryptonote::COMMAND_RPC_GET_OUTPUT_HISTOGRAM::entry &e2)->bool { return e1.instances < e2.instances; }); + for (const auto &e: res.histogram) + { + tools::msg_writer() << e.instances << " " << cryptonote::print_money(e.amount); + } + + return true; +} + }// namespace daemonize diff --git a/src/daemon/rpc_command_executor.h b/src/daemon/rpc_command_executor.h index bc3f2d5c5..7e73e7faf 100644 --- a/src/daemon/rpc_command_executor.h +++ b/src/daemon/rpc_command_executor.h @@ -132,6 +132,8 @@ public: bool unban(const std::string &ip); bool flush_txpool(const std::string &txid); + + bool output_histogram(uint64_t min_count, uint64_t max_count); }; } // namespace daemonize diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index 6b625e77b..5350b6fe0 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -1041,6 +1041,38 @@ namespace cryptonote return true; } //------------------------------------------------------------------------------------------------------------------------------ + bool core_rpc_server::on_get_output_histogram(const COMMAND_RPC_GET_OUTPUT_HISTOGRAM::request& req, COMMAND_RPC_GET_OUTPUT_HISTOGRAM::response& res, epee::json_rpc::error& error_resp) + { + if(!check_core_busy()) + { + error_resp.code = CORE_RPC_ERROR_CODE_CORE_BUSY; + error_resp.message = "Core is busy."; + return false; + } + + std::map<uint64_t, uint64_t> histogram; + try + { + histogram = m_core.get_blockchain_storage().get_output_histogram(req.amounts); + } + catch (const std::exception &e) + { + res.status = "Failed to get output histogram"; + return true; + } + + res.histogram.clear(); + res.histogram.reserve(histogram.size()); + for (const auto &i: histogram) + { + if (i.second >= req.min_count && (i.second <= req.max_count || req.max_count == 0)) + res.histogram.push_back(COMMAND_RPC_GET_OUTPUT_HISTOGRAM::entry(i.first, i.second)); + } + + res.status = CORE_RPC_STATUS_OK; + return true; + } + //------------------------------------------------------------------------------------------------------------------------------ bool core_rpc_server::on_fast_exit(const COMMAND_RPC_FAST_EXIT::request& req, COMMAND_RPC_FAST_EXIT::response& res) { cryptonote::core::set_fast_exit(); diff --git a/src/rpc/core_rpc_server.h b/src/rpc/core_rpc_server.h index f79087030..5c3707209 100644 --- a/src/rpc/core_rpc_server.h +++ b/src/rpc/core_rpc_server.h @@ -109,6 +109,7 @@ namespace cryptonote MAP_JON_RPC_WE("setbans", on_set_bans, COMMAND_RPC_SETBANS) MAP_JON_RPC_WE("getbans", on_get_bans, COMMAND_RPC_GETBANS) MAP_JON_RPC_WE("flush_txpool", on_flush_txpool, COMMAND_RPC_FLUSH_TRANSACTION_POOL) + MAP_JON_RPC_WE("get_output_histogram", on_get_output_histogram, COMMAND_RPC_GET_OUTPUT_HISTOGRAM) END_JSON_RPC_MAP() END_URI_MAP2() @@ -149,6 +150,7 @@ namespace cryptonote bool on_set_bans(const COMMAND_RPC_SETBANS::request& req, COMMAND_RPC_SETBANS::response& res, epee::json_rpc::error& error_resp); bool on_get_bans(const COMMAND_RPC_GETBANS::request& req, COMMAND_RPC_GETBANS::response& res, epee::json_rpc::error& error_resp); bool on_flush_txpool(const COMMAND_RPC_FLUSH_TRANSACTION_POOL::request& req, COMMAND_RPC_FLUSH_TRANSACTION_POOL::response& res, epee::json_rpc::error& error_resp); + bool on_get_output_histogram(const COMMAND_RPC_GET_OUTPUT_HISTOGRAM::request& req, COMMAND_RPC_GET_OUTPUT_HISTOGRAM::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 72e399ec4..6d4dd1252 100644 --- a/src/rpc/core_rpc_server_commands_defs.h +++ b/src/rpc/core_rpc_server_commands_defs.h @@ -986,5 +986,46 @@ namespace cryptonote END_KV_SERIALIZE_MAP() }; }; + + struct COMMAND_RPC_GET_OUTPUT_HISTOGRAM + { + struct request + { + std::vector<uint64_t> amounts; + uint64_t min_count; + uint64_t max_count; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(amounts); + KV_SERIALIZE(min_count); + KV_SERIALIZE(max_count); + END_KV_SERIALIZE_MAP() + }; + + struct entry + { + uint64_t amount; + uint64_t instances; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(amount); + KV_SERIALIZE(instances); + END_KV_SERIALIZE_MAP() + + entry(uint64_t amount, uint64_t instances): amount(amount), instances(instances) {} + entry() {} + }; + + struct response + { + std::string status; + std::vector<entry> histogram; + + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(status) + KV_SERIALIZE(histogram) + END_KV_SERIALIZE_MAP() + }; + }; } diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index 56c6ec8a6..46e705673 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -541,7 +541,7 @@ simple_wallet::simple_wallet() m_cmd_binder.set_handler("bc_height", boost::bind(&simple_wallet::show_blockchain_height, this, _1), tr("Show blockchain height")); m_cmd_binder.set_handler("transfer", boost::bind(&simple_wallet::transfer, this, _1), tr("transfer [<mixin_count>] <addr_1> <amount_1> [<addr_2> <amount_2> ... <addr_N> <amount_N>] [payment_id] - Transfer <amount_1>,... <amount_N> to <address_1>,... <address_N>, respectively. <mixin_count> is the number of extra inputs to include for untraceability (from 0 to maximum available)")); m_cmd_binder.set_handler("transfer_new", boost::bind(&simple_wallet::transfer_new, this, _1), tr("Same as transfer, but using a new transaction building algorithm")); - m_cmd_binder.set_handler("sweep_dust", boost::bind(&simple_wallet::sweep_dust, this, _1), tr("Send all dust outputs to yourself with mixin 0")); + m_cmd_binder.set_handler("sweep_unmixable", boost::bind(&simple_wallet::sweep_unmixable, this, _1), tr("Send all unmixable outputs to yourself with mixin 0")); m_cmd_binder.set_handler("set_log", boost::bind(&simple_wallet::set_log, this, _1), tr("set_log <level> - Change current log detail level, <0-4>")); m_cmd_binder.set_handler("address", boost::bind(&simple_wallet::print_address, this, _1), tr("Show current wallet public address")); m_cmd_binder.set_handler("integrated_address", boost::bind(&simple_wallet::print_integrated_address, this, _1), tr("integrated_address [PID] - Encode a payment ID into an integrated address for the current wallet public address (no argument uses a random payment ID), or decode an integrated address to standard address and payment ID")); @@ -1764,8 +1764,7 @@ bool simple_wallet::refresh(const std::vector<std::string>& args) bool simple_wallet::show_balance(const std::vector<std::string>& args/* = std::vector<std::string>()*/) { success_msg_writer() << tr("Balance: ") << print_money(m_wallet->balance()) << ", " - << tr("unlocked balance: ") << print_money(m_wallet->unlocked_balance()) << ", " - << tr("including unlocked dust: ") << print_money(m_wallet->unlocked_dust_balance(tools::tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD))); + << tr("unlocked balance: ") << print_money(m_wallet->unlocked_balance()); return true; } //---------------------------------------------------------------------------------------------------- @@ -2250,7 +2249,7 @@ bool simple_wallet::transfer_new(const std::vector<std::string> &args_) } //---------------------------------------------------------------------------------------------------- -bool simple_wallet::sweep_dust(const std::vector<std::string> &args_) +bool simple_wallet::sweep_unmixable(const std::vector<std::string> &args_) { if (!try_connect_to_daemon()) return true; @@ -2263,28 +2262,37 @@ bool simple_wallet::sweep_dust(const std::vector<std::string> &args_) try { - uint64_t total_dust = m_wallet->unlocked_dust_balance(tools::tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD)); - // figure out what tx will be necessary - auto ptx_vector = m_wallet->create_dust_sweep_transactions(); + auto ptx_vector = m_wallet->create_unmixable_sweep_transactions(m_trusted_daemon); + + if (ptx_vector.empty()) + { + fail_msg_writer() << tr("No unmixable outputs found"); + return true; + } // give user total and fee, and prompt to confirm - uint64_t total_fee = 0; + uint64_t total_fee = 0, total_unmixable = 0; for (size_t n = 0; n < ptx_vector.size(); ++n) { total_fee += ptx_vector[n].fee; + for (const auto &vin: ptx_vector[n].tx.vin) + { + if (vin.type() == typeid(txin_to_key)) + total_unmixable += boost::get<txin_to_key>(vin).amount; + } } - std::string prompt_str = tr("Sweeping ") + print_money(total_dust); + std::string prompt_str = tr("Sweeping ") + print_money(total_unmixable); if (ptx_vector.size() > 1) { prompt_str = (boost::format(tr("Sweeping %s in %llu transactions for a total fee of %s. Is this okay? (Y/Yes/N/No)")) % - print_money(total_dust) % + print_money(total_unmixable) % ((unsigned long long)ptx_vector.size()) % print_money(total_fee)).str(); } else { prompt_str = (boost::format(tr("Sweeping %s for a total fee of %s. Is this okay? (Y/Yes/N/No)")) % - print_money(total_dust) % + print_money(total_unmixable) % print_money(total_fee)).str(); } std::string accepted = command_line::input_line(prompt_str); @@ -2327,11 +2335,12 @@ bool simple_wallet::sweep_dust(const std::vector<std::string> &args_) } catch (const tools::error::not_enough_money& e) { - fail_msg_writer() << boost::format(tr("not enough money to transfer, available only %s, transaction amount %s = %s + %s (fee)")) % + fail_msg_writer() << boost::format(tr("not enough money to transfer, available only %s, transaction amount %s = %s + %s (fee).\n%s")) % print_money(e.available()) % print_money(e.tx_amount() + e.fee()) % print_money(e.tx_amount()) % - print_money(e.fee()); + print_money(e.fee()) % + tr("This is usually due to dust which is so small it cannot pay for itself in fees"); } catch (const tools::error::not_enough_outs_to_mix& e) { diff --git a/src/simplewallet/simplewallet.h b/src/simplewallet/simplewallet.h index 7dadb8536..21bbfa566 100644 --- a/src/simplewallet/simplewallet.h +++ b/src/simplewallet/simplewallet.h @@ -121,7 +121,7 @@ namespace cryptonote bool transfer_main(bool new_algorithm, const std::vector<std::string> &args); bool transfer(const std::vector<std::string> &args); bool transfer_new(const std::vector<std::string> &args); - bool sweep_dust(const std::vector<std::string> &args); + bool sweep_unmixable(const std::vector<std::string> &args); std::vector<std::vector<cryptonote::tx_destination_entry>> split_amounts( std::vector<cryptonote::tx_destination_entry> dsts, size_t num_splits ); diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 1c7187cf0..363665024 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -92,6 +92,13 @@ void do_prepare_file_names(const std::string& file_path, std::string& keys_file, } } +uint64_t calculate_fee(const cryptonote::blobdata &blob) +{ + uint64_t bytes = blob.size(); + uint64_t kB = (bytes + 1023) / 1024; + return kB * FEE_PER_KB; +} + } //namespace namespace tools @@ -1207,7 +1214,7 @@ void wallet2::generate(const std::string& wallet_, const std::string& password, m_account_public_address = account_public_address; m_watch_only = false; - bool r = store_keys(m_keys_file, password, true); + bool r = store_keys(m_keys_file, password, false); THROW_WALLET_EXCEPTION_IF(!r, error::file_save_error, m_keys_file); r = file_io_utils::save_string_to_file(m_wallet_file + ".address.txt", m_account.get_public_address_str(m_testnet)); @@ -2030,13 +2037,7 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions(std::vector<crypto { transfer(dst_vector, fake_outs_count, unlock_time, needed_fee, extra, tx, ptx); auto txBlob = t_serializable_object_to_blob(ptx.tx); - uint64_t txSize = txBlob.size(); - uint64_t numKB = txSize / 1024; - if (txSize % 1024) - { - numKB++; - } - needed_fee = numKB * FEE_PER_KB; + needed_fee = calculate_fee(txBlob); } while (ptx.fee < needed_fee); ptx_vector.push_back(ptx); @@ -2408,15 +2409,9 @@ std::vector<wallet2::pending_tx> wallet2::create_transactions_2(std::vector<cryp transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, unlock_time, needed_fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx); auto txBlob = t_serializable_object_to_blob(test_ptx.tx); - uint64_t txSize = txBlob.size(); - uint64_t numKB = txSize / 1024; - if (txSize % 1024) - { - numKB++; - } - needed_fee = numKB * FEE_PER_KB; + needed_fee = calculate_fee(txBlob); available_for_fee = test_ptx.fee + test_ptx.change_dts.amount; - LOG_PRINT_L2("Made a " << numKB << " kB tx, with " << print_money(available_for_fee) << " available for fee (" << + LOG_PRINT_L2("Made a " << txBlob.size() << " kB tx, with " << print_money(available_for_fee) << " available for fee (" << print_money(needed_fee) << " needed)"); if (needed_fee > available_for_fee && dsts[0].amount > 0) @@ -2513,7 +2508,7 @@ uint64_t wallet2::unlocked_dust_balance(const tx_dust_policy &dust_policy) const } template<typename T> -void wallet2::transfer_dust(size_t num_outputs, uint64_t unlock_time, uint64_t needed_fee, T destination_split_strategy, const tx_dust_policy& dust_policy, const std::vector<uint8_t> &extra, cryptonote::transaction& tx, pending_tx &ptx) +void wallet2::transfer_from(const std::vector<size_t> &outs, size_t num_outputs, uint64_t unlock_time, uint64_t needed_fee, T destination_split_strategy, const tx_dust_policy& dust_policy, const std::vector<uint8_t> &extra, cryptonote::transaction& tx, pending_tx &ptx) { using namespace cryptonote; @@ -2523,6 +2518,19 @@ void wallet2::transfer_dust(size_t num_outputs, uint64_t unlock_time, uint64_t n // throw if there are none uint64_t money = 0; std::list<transfer_container::iterator> selected_transfers; +#if 1 + for (size_t n = 0; n < outs.size(); ++n) + { + const transfer_details& td = m_transfers[outs[n]]; + if (!td.m_spent) + { + selected_transfers.push_back (m_transfers.begin() + outs[n]); + money += td.amount(); + if (selected_transfers.size() >= num_outputs) + break; + } + } +#else for (transfer_container::iterator i = m_transfers.begin(); i != m_transfers.end(); ++i) { const transfer_details& td = *i; @@ -2534,6 +2542,7 @@ void wallet2::transfer_dust(size_t num_outputs, uint64_t unlock_time, uint64_t n break; } } +#endif // we don't allow no output to self, easier, but one may want to burn the dust if = fee THROW_WALLET_EXCEPTION_IF(money <= needed_fee, error::not_enough_money, money, needed_fee, needed_fee); @@ -2627,8 +2636,8 @@ bool wallet2::use_fork_rules(uint8_t version) r = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/json_rpc", req_t, resp_t, m_http_client); m_daemon_rpc_mutex.unlock(); CHECK_AND_ASSERT_MES(r, false, "Failed to connect to daemon"); - CHECK_AND_ASSERT_MES(res.status != CORE_RPC_STATUS_BUSY, false, "Failed to connect to daemon"); - CHECK_AND_ASSERT_MES(res.status == CORE_RPC_STATUS_OK, false, "Failed to get hard fork status"); + CHECK_AND_ASSERT_MES(resp_t.result.status != CORE_RPC_STATUS_BUSY, false, "Failed to connect to daemon"); + CHECK_AND_ASSERT_MES(resp_t.result.status == CORE_RPC_STATUS_OK, false, "Failed to get hard fork status"); bool close_enough = res.height >= resp_t.result.earliest_height - 10; // start using the rules a bit beforehand if (close_enough) @@ -2646,20 +2655,85 @@ uint64_t wallet2::get_upper_tranaction_size_limit() return ((full_reward_zone * 125) / 100) - CRYPTONOTE_COINBASE_BLOB_RESERVED_SIZE; } //---------------------------------------------------------------------------------------------------- -std::vector<wallet2::pending_tx> wallet2::create_dust_sweep_transactions() +std::vector<size_t> wallet2::select_available_outputs(const std::function<bool(const transfer_details &td)> &f) +{ + std::vector<size_t> outputs; + size_t n = 0; + for (transfer_container::const_iterator i = m_transfers.begin(); i != m_transfers.end(); ++i, ++n) + { + if (i->m_spent) + continue; + if (!is_transfer_unlocked(*i)) + continue; + if (f(*i)) + outputs.push_back(n); + } + return outputs; +} +//---------------------------------------------------------------------------------------------------- +std::vector<uint64_t> wallet2::get_unspent_amounts_vector() +{ + std::set<uint64_t> set; + for (const auto &td: m_transfers) + { + if (!td.m_spent) + set.insert(td.amount()); + } + std::vector<uint64_t> vector; + vector.reserve(set.size()); + for (const auto &i: set) + { + vector.push_back(i); + } + return vector; +} +//---------------------------------------------------------------------------------------------------- +std::vector<size_t> wallet2::select_available_unmixable_outputs(bool trusted_daemon) +{ + // request all outputs with at least 3 instances, so we can use mixin 2 with + epee::json_rpc::request<cryptonote::COMMAND_RPC_GET_OUTPUT_HISTOGRAM::request> req_t = AUTO_VAL_INIT(req_t); + epee::json_rpc::response<cryptonote::COMMAND_RPC_GET_OUTPUT_HISTOGRAM::response, std::string> resp_t = AUTO_VAL_INIT(resp_t); + m_daemon_rpc_mutex.lock(); + req_t.jsonrpc = "2.0"; + req_t.id = epee::serialization::storage_entry(0); + req_t.method = "get_output_histogram"; + if (trusted_daemon) + req_t.params.amounts = get_unspent_amounts_vector(); + req_t.params.min_count = 3; + req_t.params.max_count = 0; + bool r = net_utils::invoke_http_json_remote_command2(m_daemon_address + "/json_rpc", req_t, resp_t, m_http_client); + m_daemon_rpc_mutex.unlock(); + THROW_WALLET_EXCEPTION_IF(!r, error::no_connection_to_daemon, "select_available_unmixable_outputs"); + THROW_WALLET_EXCEPTION_IF(resp_t.result.status == CORE_RPC_STATUS_BUSY, error::daemon_busy, "get_output_histogram"); + THROW_WALLET_EXCEPTION_IF(resp_t.result.status != CORE_RPC_STATUS_OK, error::get_histogram_error, resp_t.result.status); + + std::set<uint64_t> mixable; + for (const auto &i: resp_t.result.histogram) + { + mixable.insert(i.amount); + } + + return select_available_outputs([mixable](const transfer_details &td) { + const uint64_t amount = td.amount(); + if (mixable.find(amount) == mixable.end()) + return true; + return false; + }); +} +//---------------------------------------------------------------------------------------------------- +std::vector<wallet2::pending_tx> wallet2::create_unmixable_sweep_transactions(bool trusted_daemon) { // From hard fork 1, we don't consider small amounts to be dust anymore const bool hf1_rules = use_fork_rules(2); // first hard fork has version 2 tx_dust_policy dust_policy(hf1_rules ? 0 : ::config::DEFAULT_DUST_THRESHOLD); - size_t num_dust_outputs = 0; - for (transfer_container::const_iterator i = m_transfers.begin(); i != m_transfers.end(); ++i) + // may throw + std::vector<size_t> unmixable_outputs = select_available_unmixable_outputs(trusted_daemon); + size_t num_dust_outputs = unmixable_outputs.size(); + + if (num_dust_outputs == 0) { - const transfer_details& td = *i; - if (!td.m_spent && (td.amount() < dust_policy.dust_threshold || !is_valid_decomposed_amount(td.amount())) && is_transfer_unlocked(td)) - { - num_dust_outputs++; - } + return std::vector<wallet2::pending_tx>(); } // failsafe split attempt counter @@ -2682,23 +2756,18 @@ std::vector<wallet2::pending_tx> wallet2::create_dust_sweep_transactions() // loop until fee is met without increasing tx size to next KB boundary. uint64_t needed_fee = 0; - if (1) + do { - transfer_dust(num_outputs_per_tx, (uint64_t)0 /* unlock_time */, 0, detail::digit_split_strategy, dust_policy, extra, tx, ptx); + transfer_from(unmixable_outputs, num_outputs_per_tx, (uint64_t)0 /* unlock_time */, 0, detail::digit_split_strategy, dust_policy, extra, tx, ptx); auto txBlob = t_serializable_object_to_blob(ptx.tx); - uint64_t txSize = txBlob.size(); - uint64_t numKB = txSize / 1024; - if (txSize % 1024) - { - numKB++; - } - needed_fee = numKB * FEE_PER_KB; + needed_fee = calculate_fee(txBlob); // reroll the tx with the actual amount minus the fee // if there's not enough for the fee, it'll throw - transfer_dust(num_outputs_per_tx, (uint64_t)0 /* unlock_time */, needed_fee, detail::digit_split_strategy, dust_policy, extra, tx, ptx); + transfer_from(unmixable_outputs, num_outputs_per_tx, (uint64_t)0 /* unlock_time */, needed_fee, detail::digit_split_strategy, dust_policy, extra, tx, ptx); txBlob = t_serializable_object_to_blob(ptx.tx); - } + needed_fee = calculate_fee(txBlob); + } while (ptx.fee < needed_fee); ptx_vector.push_back(ptx); diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index e7b002925..fc700a3de 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -280,7 +280,7 @@ namespace tools void transfer(const std::vector<cryptonote::tx_destination_entry>& dsts, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra); void transfer(const std::vector<cryptonote::tx_destination_entry>& dsts, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, cryptonote::transaction& tx, pending_tx& ptx); template<typename T> - void transfer_dust(size_t num_outputs, uint64_t unlock_time, uint64_t needed_fee, T destination_split_strategy, const tx_dust_policy& dust_policy, const std::vector<uint8_t>& extra, cryptonote::transaction& tx, pending_tx &ptx); + void transfer_from(const std::vector<size_t> &outs, size_t num_outputs, uint64_t unlock_time, uint64_t needed_fee, T destination_split_strategy, const tx_dust_policy& dust_policy, const std::vector<uint8_t>& extra, cryptonote::transaction& tx, pending_tx &ptx); template<typename T> void transfer_selected(const std::vector<cryptonote::tx_destination_entry>& dsts, const std::list<transfer_container::iterator> selected_transfers, size_t fake_outputs_count, uint64_t unlock_time, uint64_t fee, const std::vector<uint8_t>& extra, T destination_split_strategy, const tx_dust_policy& dust_policy, cryptonote::transaction& tx, pending_tx &ptx); @@ -289,7 +289,7 @@ namespace tools void commit_tx(std::vector<pending_tx>& ptx_vector); std::vector<pending_tx> create_transactions(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee, const std::vector<uint8_t> extra); std::vector<wallet2::pending_tx> create_transactions_2(std::vector<cryptonote::tx_destination_entry> dsts, const size_t fake_outs_count, const uint64_t unlock_time, const uint64_t fee_UNUSED, const std::vector<uint8_t> extra); - std::vector<pending_tx> create_dust_sweep_transactions(); + std::vector<pending_tx> create_unmixable_sweep_transactions(bool trusted_daemon); bool check_connection(); void get_transfers(wallet2::transfer_container& incoming_transfers) const; void get_payments(const crypto::hash& payment_id, std::list<wallet2::payment_details>& payments, uint64_t min_height = 0) const; @@ -402,6 +402,9 @@ namespace tools void parse_block_round(const cryptonote::blobdata &blob, cryptonote::block &bl, crypto::hash &bl_id, bool &error) const; uint64_t get_upper_tranaction_size_limit(); void check_pending_txes(); + std::vector<uint64_t> get_unspent_amounts_vector(); + std::vector<size_t> select_available_outputs(const std::function<bool(const transfer_details &td)> &f); + std::vector<size_t> select_available_unmixable_outputs(bool trusted_daemon); cryptonote::account_base m_account; std::string m_daemon_address; diff --git a/src/wallet/wallet_errors.h b/src/wallet/wallet_errors.h index 6074e0858..652b19499 100644 --- a/src/wallet/wallet_errors.h +++ b/src/wallet/wallet_errors.h @@ -76,6 +76,7 @@ namespace tools // daemon_busy // no_connection_to_daemon // is_key_image_spent_error + // get_histogram_error // wallet_files_doesnt_correspond // // * - class with protected ctor @@ -600,6 +601,14 @@ namespace tools } }; //---------------------------------------------------------------------------------------------------- + struct get_histogram_error : public wallet_rpc_error + { + explicit get_histogram_error(std::string&& loc, const std::string& request) + : wallet_rpc_error(std::move(loc), "failed to get output histogram", 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/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 418de327c..d7d99c2ae 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -347,7 +347,7 @@ namespace tools try { - std::vector<wallet2::pending_tx> ptx_vector = m_wallet.create_dust_sweep_transactions(); + std::vector<wallet2::pending_tx> ptx_vector = m_wallet.create_unmixable_sweep_transactions(req.trusted_daemon); m_wallet.commit_tx(ptx_vector); diff --git a/src/wallet/wallet_rpc_server_commands_defs.h b/src/wallet/wallet_rpc_server_commands_defs.h index 40d6fd8f8..2c4e26406 100644 --- a/src/wallet/wallet_rpc_server_commands_defs.h +++ b/src/wallet/wallet_rpc_server_commands_defs.h @@ -178,9 +178,11 @@ namespace wallet_rpc struct request { bool get_tx_keys; + bool trusted_daemon; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE(get_tx_keys) + KV_SERIALIZE(trusted_daemon) END_KV_SERIALIZE_MAP() }; diff --git a/tests/unit_tests/hardfork.cpp b/tests/unit_tests/hardfork.cpp index 50e0e5ae8..cc3eba8ea 100644 --- a/tests/unit_tests/hardfork.cpp +++ b/tests/unit_tests/hardfork.cpp @@ -108,6 +108,7 @@ public: virtual bool for_all_transactions(std::function<bool(const crypto::hash&, const cryptonote::transaction&)>) const { return true; } virtual bool for_all_outputs(std::function<bool(uint64_t amount, const crypto::hash &tx_hash, size_t tx_idx)> f) const { return true; } virtual bool is_read_only() const { return false; } + virtual std::map<uint64_t, uint64_t> get_output_histogram(const std::vector<uint64_t> &amounts) const { return std::map<uint64_t, uint64_t>(); } virtual void add_block( const block& blk , const size_t& block_size |