From b030f207517f59a5122409398549a02ac23829ae Mon Sep 17 00:00:00 2001 From: moneromooo-monero Date: Thu, 29 Jul 2021 12:02:08 +0000 Subject: [PATCH] Fee changes from ArticMine https://github.com/ArticMine/Monero-Documents/blob/master/MoneroScaling2021-02.pdf with a change to use 1.7 instead of 2.0 for the max long term increase rate --- .../cryptonote_format_utils.cpp | 63 ++++++ .../cryptonote_format_utils.h | 2 + src/cryptonote_config.h | 2 + src/cryptonote_core/blockchain.cpp | 137 ++++++++++++- src/cryptonote_core/blockchain.h | 16 ++ src/rpc/core_rpc_server.cpp | 12 +- src/rpc/core_rpc_server_commands_defs.h | 2 + src/wallet/api/wallet.cpp | 1 - src/wallet/node_rpc_proxy.cpp | 16 +- src/wallet/node_rpc_proxy.h | 2 + src/wallet/wallet2.cpp | 94 ++++++--- src/wallet/wallet2.h | 3 +- tests/functional_tests/blockchain.py | 8 +- tests/unit_tests/CMakeLists.txt | 1 + tests/unit_tests/scaling_2021.cpp | 187 ++++++++++++++++++ 15 files changed, 498 insertions(+), 48 deletions(-) create mode 100644 tests/unit_tests/scaling_2021.cpp diff --git a/src/cryptonote_basic/cryptonote_format_utils.cpp b/src/cryptonote_basic/cryptonote_format_utils.cpp index 1bc6b6377..d8f9bb176 100644 --- a/src/cryptonote_basic/cryptonote_format_utils.cpp +++ b/src/cryptonote_basic/cryptonote_format_utils.cpp @@ -1064,6 +1064,69 @@ namespace cryptonote return s; } //--------------------------------------------------------------- + uint64_t round_money_up(uint64_t amount, unsigned significant_digits) + { + // round monetary amount up with the requested amount of significant digits + + CHECK_AND_ASSERT_THROW_MES(significant_digits > 0, "significant_digits must not be 0"); + static_assert(sizeof(unsigned long long) == sizeof(uint64_t), "Unexpected unsigned long long size"); + + // we don't need speed, so we do it via text, as it's easier to get right + char buf[32]; + snprintf(buf, sizeof(buf), "%llu", (unsigned long long)amount); + const size_t len = strlen(buf); + if (len > significant_digits) + { + bool bump = false; + char *ptr; + for (ptr = buf + significant_digits; *ptr; ++ptr) + { + // bump digits by one if the following digits past significant digits were to be 5 or more + if (*ptr != '0') + { + bump = true; + *ptr = '0'; + } + } + ptr = buf + significant_digits; + while (bump && ptr > buf) + { + --ptr; + // bumping a nine overflows + if (*ptr == '9') + *ptr = '0'; + else + { + // bumping another digit means we can stop bumping (no carry) + ++*ptr; + bump = false; + } + } + if (bump) + { + // carry reached the highest digit, we need to add a 1 in front + size_t offset = strlen(buf); + for (size_t i = offset + 1; i > 0; --i) + buf[i] = buf[i - 1]; + buf[0] = '1'; + } + } + char *end = NULL; + errno = 0; + const unsigned long long ull = strtoull(buf, &end, 10); + CHECK_AND_ASSERT_THROW_MES(ull != ULONG_MAX || errno == 0, "Failed to parse rounded amount: " << buf); + CHECK_AND_ASSERT_THROW_MES(ull != 0 || amount == 0, "Overflow in rounding"); + return ull; + } + //--------------------------------------------------------------- + std::string round_money_up(const std::string &s, unsigned significant_digits) + { + uint64_t amount; + CHECK_AND_ASSERT_THROW_MES(parse_amount(amount, s), "Failed to parse amount: " << s); + amount = round_money_up(amount, significant_digits); + return print_money(amount); + } + //--------------------------------------------------------------- crypto::hash get_blob_hash(const blobdata& blob) { crypto::hash h = null_hash; diff --git a/src/cryptonote_basic/cryptonote_format_utils.h b/src/cryptonote_basic/cryptonote_format_utils.h index 25cd89e99..ce3ce7492 100644 --- a/src/cryptonote_basic/cryptonote_format_utils.h +++ b/src/cryptonote_basic/cryptonote_format_utils.h @@ -144,6 +144,8 @@ namespace cryptonote std::string get_unit(unsigned int decimal_point = -1); std::string print_money(uint64_t amount, unsigned int decimal_point = -1); std::string print_money(const boost::multiprecision::uint128_t &amount, unsigned int decimal_point = -1); + uint64_t round_money_up(uint64_t amount, unsigned significant_digits); + std::string round_money_up(const std::string &amount, unsigned significant_digits); //--------------------------------------------------------------- template bool t_serializable_object_from_blob(t_object& to, const blobdata& b_blob) diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index 5aee87897..8621e1696 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -183,8 +183,10 @@ #define HF_VERSION_CLSAG 13 #define HF_VERSION_DETERMINISTIC_UNLOCK_TIME 13 #define HF_VERSION_BULLETPROOF_PLUS 15 +#define HF_VERSION_2021_SCALING 15 #define PER_KB_FEE_QUANTIZATION_DECIMALS 8 +#define CRYPTONOTE_SCALING_2021_FEE_ROUNDING_PLACES 2 #define HASH_OF_HASHES_STEP 512 diff --git a/src/cryptonote_core/blockchain.cpp b/src/cryptonote_core/blockchain.cpp index 4eaca039c..5f9db676c 100644 --- a/src/cryptonote_core/blockchain.cpp +++ b/src/cryptonote_core/blockchain.cpp @@ -3710,11 +3710,24 @@ uint64_t Blockchain::get_dynamic_base_fee(uint64_t block_reward, size_t median_b if (version >= HF_VERSION_PER_BYTE_FEE) { lo = mul128(block_reward, DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT, &hi); - div128_64(hi, lo, min_block_weight, &hi, &lo, NULL, NULL); div128_64(hi, lo, median_block_weight, &hi, &lo, NULL, NULL); - assert(hi == 0); - lo /= 5; - return lo; + if (version >= HF_VERSION_2021_SCALING) + { + // min_fee_per_byte = round_up( 0.95 * block_reward * ref_weight / (fee_median^2) ) + // note: since hardfork HF_VERSION_2021_SCALING, fee_median (a.k.a. median_block_weight) equals effective long term median + div128_64(hi, lo, median_block_weight, &hi, &lo, NULL, NULL); + assert(hi == 0); + lo -= lo / 20; + return lo; + } + else + { + // min_fee_per_byte = 0.2 * block_reward * ref_weight / (min_penalty_free_zone * fee_median) + div128_64(hi, lo, min_block_weight, &hi, &lo, NULL, NULL); + assert(hi == 0); + lo /= 5; + return lo; + } } const uint64_t fee_base = version >= 5 ? DYNAMIC_FEE_PER_KB_BASE_FEE_V5 : DYNAMIC_FEE_PER_KB_BASE_FEE; @@ -3786,6 +3799,81 @@ bool Blockchain::check_fee(size_t tx_weight, uint64_t fee) const return true; } +//------------------------------------------------------------------ +void Blockchain::get_dynamic_base_fee_estimate_2021_scaling(uint64_t grace_blocks, uint64_t base_reward, uint64_t Mnw, uint64_t Mlw, std::vector &fees) const +{ + // variable names and calculations as per https://github.com/ArticMine/Monero-Documents/blob/master/MoneroScaling2021-02.pdf + // from (earlier than) this fork, the base fee is per byte + const uint64_t Mfw = std::min(Mnw, Mlw); + + // 3 kB divided by something ? It's going to be either 0 or *very* quantized, so fold it into integer steps below + //const uint64_t Brlw = DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT / Mfw; + + // constant.... equal to 0, unless floating point, so fold it into integer steps below + //const uint64_t Br = DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT / CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 + + //const uint64_t Fl = base_reward * Brlw / Mfw; fold Brlw from above + const uint64_t Fl = base_reward * /*Brlw*/ DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT / (Mfw * Mfw); + + // fold Fl into this for better precision (and to match the test cases in the PDF) + // const uint64_t Fn = 4 * Fl; + const uint64_t Fn = 4 * base_reward * /*Brlw*/ DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT / (Mfw * Mfw); + + // const uint64_t Fm = 16 * base_reward * Br / Mfw; fold Br from above + const uint64_t Fm = 16 * base_reward * DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT / (CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5 * Mfw); + + // const uint64_t Fp = 2 * base_reward / Mnw; + + // fold Br from above, move 4Fm in the max to decrease quantization effect + //const uint64_t Fh = 4 * Fm * std::max(1, Mfw / (32 * DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT * Mnw / CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5)); + const uint64_t Fh = std::max(4 * Fm, 4 * Fm * Mfw / (32 * DYNAMIC_FEE_REFERENCE_TRANSACTION_WEIGHT * Mnw / CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5)); + + fees.resize(4); + fees[0] = cryptonote::round_money_up(Fl, CRYPTONOTE_SCALING_2021_FEE_ROUNDING_PLACES); + fees[1] = cryptonote::round_money_up(Fn, CRYPTONOTE_SCALING_2021_FEE_ROUNDING_PLACES); + fees[2] = cryptonote::round_money_up(Fm, CRYPTONOTE_SCALING_2021_FEE_ROUNDING_PLACES); + fees[3] = cryptonote::round_money_up(Fh, CRYPTONOTE_SCALING_2021_FEE_ROUNDING_PLACES); +} + +void Blockchain::get_dynamic_base_fee_estimate_2021_scaling(uint64_t grace_blocks, std::vector &fees) const +{ + const uint8_t version = get_current_hard_fork_version(); + const uint64_t db_height = m_db->height(); + + // we want Mlw = median of max((min(Mbw, 1.7 * Ml), Zm), Ml / 1.7) + // Mbw: block weight for the last 99990 blocks, 0 for the next 10 + // Ml: penalty free zone (dynamic), aka long_term_median, aka median of max((min(Mb, 1.7 * Ml), Zm), Ml / 1.7) + // Zm: 300000 (minimum penalty free zone) + // + // So we copy the current rolling median state, add 10 (grace_blocks) zeroes to it, and get back Mlw + + epee::misc_utils::rolling_median_t rm = m_long_term_block_weights_cache_rolling_median; + for (size_t i = 0; i < grace_blocks; ++i) + rm.insert(0); + const uint64_t Mlw_penalty_free_zone_for_wallet = std::max(rm.median(), CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5); + + // Msw: median over [100 - grace blocks] past + [grace blocks] future blocks + CHECK_AND_ASSERT_THROW_MES(grace_blocks <= 100, "Grace blocks invalid In 2021 fee scaling estimate."); + std::vector weights; + get_last_n_blocks_weights(weights, 100 - grace_blocks); + weights.reserve(100); + for (size_t i = 0; i < grace_blocks; ++i) + weights.push_back(0); + const uint64_t Msw_effective_short_term_median = std::max(epee::misc_utils::median(weights), Mlw_penalty_free_zone_for_wallet); + + const uint64_t Mnw = std::min(Msw_effective_short_term_median, 50 * Mlw_penalty_free_zone_for_wallet); + + uint64_t already_generated_coins = db_height ? m_db->get_block_already_generated_coins(db_height - 1) : 0; + uint64_t base_reward; + if (!get_block_reward(m_current_block_cumul_weight_limit / 2, 1, already_generated_coins, base_reward, version)) + { + MERROR("Failed to determine block reward, using placeholder " << print_money(BLOCK_REWARD_OVERESTIMATE) << " as a high bound"); + base_reward = BLOCK_REWARD_OVERESTIMATE; + } + + get_dynamic_base_fee_estimate_2021_scaling(grace_blocks, base_reward, Mnw, Mlw_penalty_free_zone_for_wallet, fees); +} + //------------------------------------------------------------------ uint64_t Blockchain::get_dynamic_base_fee_estimate(uint64_t grace_blocks) const { @@ -3798,6 +3886,13 @@ uint64_t Blockchain::get_dynamic_base_fee_estimate(uint64_t grace_blocks) const if (grace_blocks >= CRYPTONOTE_REWARD_BLOCKS_WINDOW) grace_blocks = CRYPTONOTE_REWARD_BLOCKS_WINDOW - 1; + if (version >= HF_VERSION_2021_SCALING) + { + std::vector fees; + get_dynamic_base_fee_estimate_2021_scaling(grace_blocks, fees); + return fees[0]; + } + const uint64_t min_block_weight = get_min_block_weight(version); std::vector weights; get_last_n_blocks_weights(weights, CRYPTONOTE_REWARD_BLOCKS_WINDOW - grace_blocks); @@ -4472,6 +4567,7 @@ bool Blockchain::check_blockchain_pruning() return m_db->check_pruning(); } //------------------------------------------------------------------ +// returns min(Mb, 1.7*Ml) as per https://github.com/ArticMine/Monero-Documents/blob/master/MoneroScaling2021-02.pdf from HF_VERSION_LONG_TERM_BLOCK_WEIGHT uint64_t Blockchain::get_next_long_term_block_weight(uint64_t block_weight) const { PERF_TIMER(get_next_long_term_block_weight); @@ -4486,7 +4582,18 @@ uint64_t Blockchain::get_next_long_term_block_weight(uint64_t block_weight) cons uint64_t long_term_median = get_long_term_block_weight_median(db_height - nblocks, nblocks); uint64_t long_term_effective_median_block_weight = std::max(CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5, long_term_median); - uint64_t short_term_constraint = long_term_effective_median_block_weight + long_term_effective_median_block_weight * 2 / 5; + uint64_t short_term_constraint; + if (hf_version >= HF_VERSION_2021_SCALING) + { + // long_term_block_weight = block_weight bounded to range [long-term-median/1.7, long-term-median*1.7] + block_weight = std::max(block_weight, long_term_effective_median_block_weight * 10 / 17); + short_term_constraint = long_term_effective_median_block_weight + long_term_effective_median_block_weight * 7 / 10; + } + else + { + // long_term_block_weight = block_weight bounded to range [0, long-term-median*1.4] + short_term_constraint = long_term_effective_median_block_weight + long_term_effective_median_block_weight * 2 / 5; + } uint64_t long_term_block_weight = std::min(block_weight, short_term_constraint); return long_term_block_weight; @@ -4528,7 +4635,11 @@ bool Blockchain::update_next_cumulative_weight_limit(uint64_t *long_term_effecti m_long_term_effective_median_block_weight = std::max(CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5, long_term_median); - uint64_t short_term_constraint = m_long_term_effective_median_block_weight + m_long_term_effective_median_block_weight * 2 / 5; + uint64_t short_term_constraint = m_long_term_effective_median_block_weight; + if (hf_version >= HF_VERSION_2021_SCALING) + short_term_constraint += m_long_term_effective_median_block_weight * 7 / 10; + else + short_term_constraint += m_long_term_effective_median_block_weight * 2 / 5; uint64_t long_term_block_weight = std::min(block_weight, short_term_constraint); if (db_height == 1) @@ -4547,7 +4658,19 @@ bool Blockchain::update_next_cumulative_weight_limit(uint64_t *long_term_effecti get_last_n_blocks_weights(weights, CRYPTONOTE_REWARD_BLOCKS_WINDOW); uint64_t short_term_median = epee::misc_utils::median(weights); - uint64_t effective_median_block_weight = std::min(std::max(CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5, short_term_median), CRYPTONOTE_SHORT_TERM_BLOCK_WEIGHT_SURGE_FACTOR * m_long_term_effective_median_block_weight); + uint64_t effective_median_block_weight; + if (hf_version >= HF_VERSION_2021_SCALING) + { + // effective median = short_term_median bounded to range [long_term_median, 50*long_term_median], but it can't be smaller than the + // minimum penalty free zone (a.k.a. 'full reward zone') + effective_median_block_weight = std::min(std::max(m_long_term_effective_median_block_weight, short_term_median), CRYPTONOTE_SHORT_TERM_BLOCK_WEIGHT_SURGE_FACTOR * m_long_term_effective_median_block_weight); + } + else + { + // effective median = short_term_median bounded to range [0, 50*long_term_median], but it can't be smaller than the + // minimum penalty free zone (a.k.a. 'full reward zone') + effective_median_block_weight = std::min(std::max(CRYPTONOTE_BLOCK_GRANTED_FULL_REWARD_ZONE_V5, short_term_median), CRYPTONOTE_SHORT_TERM_BLOCK_WEIGHT_SURGE_FACTOR * m_long_term_effective_median_block_weight); + } m_current_block_cumul_weight_median = effective_median_block_weight; } diff --git a/src/cryptonote_core/blockchain.h b/src/cryptonote_core/blockchain.h index 5460d7761..7a94f6358 100644 --- a/src/cryptonote_core/blockchain.h +++ b/src/cryptonote_core/blockchain.h @@ -648,6 +648,22 @@ namespace cryptonote * @return the fee estimate */ uint64_t get_dynamic_base_fee_estimate(uint64_t grace_blocks) const; + void get_dynamic_base_fee_estimate_2021_scaling(uint64_t grace_blocks, uint64_t base_reward, uint64_t Mnw, uint64_t Mlw, std::vector &fees) const; + + /** + * @brief get four levels of dynamic per byte fee estimate for the next few blocks + * + * The dynamic fee is based on the block weight in a past window, and + * the current block reward. It is expressed per byte, and is based on + * https://github.com/ArticMine/Monero-Documents/blob/master/MoneroScaling2021-02.pdf + * This function calculates an estimate for a dynamic fee which will be + * valid for the next grace_blocks + * + * @param grace_blocks number of blocks we want the fee to be valid for + * + * @return the fee estimates (4 of them) + */ + void get_dynamic_base_fee_estimate_2021_scaling(uint64_t grace_blocks, std::vector &fees) const; /** * @brief validate a transaction's fee diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index d23d5cddb..b28583e37 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -2890,7 +2890,17 @@ namespace cryptonote return r; CHECK_PAYMENT(req, res, COST_PER_FEE_ESTIMATE); - res.fee = m_core.get_blockchain_storage().get_dynamic_base_fee_estimate(req.grace_blocks); + + const uint8_t version = m_core.get_blockchain_storage().get_current_hard_fork_version(); + if (version >= HF_VERSION_2021_SCALING) + { + m_core.get_blockchain_storage().get_dynamic_base_fee_estimate_2021_scaling(req.grace_blocks, res.fees); + res.fee = res.fees[0]; + } + else + { + res.fee = m_core.get_blockchain_storage().get_dynamic_base_fee_estimate(req.grace_blocks); + } res.quantization_mask = Blockchain::get_fee_quantization_mask(); res.status = CORE_RPC_STATUS_OK; return true; diff --git a/src/rpc/core_rpc_server_commands_defs.h b/src/rpc/core_rpc_server_commands_defs.h index 9206b5e21..1be2610ff 100644 --- a/src/rpc/core_rpc_server_commands_defs.h +++ b/src/rpc/core_rpc_server_commands_defs.h @@ -2191,11 +2191,13 @@ namespace cryptonote { uint64_t fee; uint64_t quantization_mask; + std::vector fees; BEGIN_KV_SERIALIZE_MAP() KV_SERIALIZE_PARENT(rpc_access_response_base) KV_SERIALIZE(fee) KV_SERIALIZE_OPT(quantization_mask, (uint64_t)1) + KV_SERIALIZE(fees) END_KV_SERIALIZE_MAP() }; typedef epee::misc_utils::struct_init response; diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp index 014aed0e7..c824e2d95 100644 --- a/src/wallet/api/wallet.cpp +++ b/src/wallet/api/wallet.cpp @@ -1744,7 +1744,6 @@ uint64_t WalletImpl::estimateTransactionFee(const std::vectoruse_fork_rules(HF_VERSION_CLSAG, 0), m_wallet->use_fork_rules(HF_VERSION_BULLETPROOF_PLUS, 0), m_wallet->get_base_fee(), - m_wallet->get_fee_multiplier(m_wallet->adjust_priority(static_cast(priority))), m_wallet->get_fee_quantization_mask()); } diff --git a/src/wallet/node_rpc_proxy.cpp b/src/wallet/node_rpc_proxy.cpp index 7635c5646..7810abdd2 100644 --- a/src/wallet/node_rpc_proxy.cpp +++ b/src/wallet/node_rpc_proxy.cpp @@ -70,6 +70,7 @@ void NodeRPCProxy::invalidate() m_dynamic_base_fee_estimate = 0; m_dynamic_base_fee_estimate_cached_height = 0; m_dynamic_base_fee_estimate_grace_blocks = 0; + m_dynamic_base_fee_estimate_vector.clear(); m_fee_quantization_mask = 1; m_rpc_version = 0; m_target_height = 0; @@ -210,7 +211,7 @@ boost::optional NodeRPCProxy::get_earliest_height(uint8_t version, return boost::optional(); } -boost::optional NodeRPCProxy::get_dynamic_base_fee_estimate(uint64_t grace_blocks, uint64_t &fee) +boost::optional NodeRPCProxy::get_dynamic_base_fee_estimate_2021_scaling(uint64_t grace_blocks, std::vector &fees) { uint64_t height; @@ -238,13 +239,24 @@ boost::optional NodeRPCProxy::get_dynamic_base_fee_estimate(uint64_ m_dynamic_base_fee_estimate = resp_t.fee; m_dynamic_base_fee_estimate_cached_height = height; m_dynamic_base_fee_estimate_grace_blocks = grace_blocks; + m_dynamic_base_fee_estimate_vector = !resp_t.fees.empty() ? std::move(resp_t.fees) : std::vector{m_dynamic_base_fee_estimate}; m_fee_quantization_mask = resp_t.quantization_mask; } - fee = m_dynamic_base_fee_estimate; + fees = m_dynamic_base_fee_estimate_vector; return boost::optional(); } +boost::optional NodeRPCProxy::get_dynamic_base_fee_estimate(uint64_t grace_blocks, uint64_t &fee) +{ + std::vector fees; + auto res = get_dynamic_base_fee_estimate_2021_scaling(grace_blocks, fees); + if (res) + return res; + fee = fees[0]; + return boost::none; +} + boost::optional NodeRPCProxy::get_fee_quantization_mask(uint64_t &fee_quantization_mask) { uint64_t height; diff --git a/src/wallet/node_rpc_proxy.h b/src/wallet/node_rpc_proxy.h index 20f4263a6..07675cdb0 100644 --- a/src/wallet/node_rpc_proxy.h +++ b/src/wallet/node_rpc_proxy.h @@ -56,6 +56,7 @@ public: boost::optional get_adjusted_time(uint64_t &adjusted_time); boost::optional get_earliest_height(uint8_t version, uint64_t &earliest_height); boost::optional get_dynamic_base_fee_estimate(uint64_t grace_blocks, uint64_t &fee); + boost::optional get_dynamic_base_fee_estimate_2021_scaling(uint64_t grace_blocks, std::vector &fees); boost::optional get_fee_quantization_mask(uint64_t &fee_quantization_mask); boost::optional get_rpc_payment_info(bool mining, bool &payment_required, uint64_t &credits, uint64_t &diff, uint64_t &credits_per_hash_found, cryptonote::blobdata &blob, uint64_t &height, uint64_t &seed_height, crypto::hash &seed_hash, crypto::hash &next_seed_hash, uint32_t &cookie); @@ -85,6 +86,7 @@ private: uint64_t m_dynamic_base_fee_estimate; uint64_t m_dynamic_base_fee_estimate_cached_height; uint64_t m_dynamic_base_fee_estimate_grace_blocks; + std::vector m_dynamic_base_fee_estimate_vector; uint64_t m_fee_quantization_mask; uint64_t m_adjusted_time; uint32_t m_rpc_version; diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index a53a1b615..7b97a44d5 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -290,15 +290,15 @@ void do_prepare_file_names(const std::string& file_path, std::string& keys_file, mms_file = file_path + ".mms"; } -uint64_t calculate_fee(uint64_t fee_per_kb, size_t bytes, uint64_t fee_multiplier) +uint64_t calculate_fee(uint64_t fee_per_kb, size_t bytes) { uint64_t kB = (bytes + 1023) / 1024; - return kB * fee_per_kb * fee_multiplier; + return kB * fee_per_kb; } -uint64_t calculate_fee_from_weight(uint64_t base_fee, uint64_t weight, uint64_t fee_multiplier, uint64_t fee_quantization_mask) +uint64_t calculate_fee_from_weight(uint64_t base_fee, uint64_t weight, uint64_t fee_quantization_mask) { - uint64_t fee = weight * base_fee * fee_multiplier; + uint64_t fee = weight * base_fee; fee = (fee + fee_quantization_mask - 1) / fee_quantization_mask * fee_quantization_mask; return fee; } @@ -878,12 +878,12 @@ uint8_t get_clsag_fork() return HF_VERSION_CLSAG; } -uint64_t calculate_fee(bool use_per_byte_fee, const cryptonote::transaction &tx, size_t blob_size, uint64_t base_fee, uint64_t fee_multiplier, uint64_t fee_quantization_mask) +uint64_t calculate_fee(bool use_per_byte_fee, const cryptonote::transaction &tx, size_t blob_size, uint64_t base_fee, uint64_t fee_quantization_mask) { if (use_per_byte_fee) - return calculate_fee_from_weight(base_fee, cryptonote::get_transaction_weight(tx, blob_size), fee_multiplier, fee_quantization_mask); + return calculate_fee_from_weight(base_fee, cryptonote::get_transaction_weight(tx, blob_size), fee_quantization_mask); else - return calculate_fee(base_fee, blob_size, fee_multiplier); + return calculate_fee(base_fee, blob_size); } bool get_short_payment_id(crypto::hash8 &payment_id8, const tools::wallet2::pending_tx &ptx, hw::device &hwdev) @@ -7232,17 +7232,17 @@ bool wallet2::sign_multisig_tx_from_file(const std::string &filename, std::vecto return sign_multisig_tx_to_file(exported_txs, filename, txids); } //---------------------------------------------------------------------------------------------------- -uint64_t wallet2::estimate_fee(bool use_per_byte_fee, bool use_rct, int n_inputs, int mixin, int n_outputs, size_t extra_size, bool bulletproof, bool clsag, bool bulletproof_plus, uint64_t base_fee, uint64_t fee_multiplier, uint64_t fee_quantization_mask) const +uint64_t wallet2::estimate_fee(bool use_per_byte_fee, bool use_rct, int n_inputs, int mixin, int n_outputs, size_t extra_size, bool bulletproof, bool clsag, bool bulletproof_plus, uint64_t base_fee, uint64_t fee_quantization_mask) const { if (use_per_byte_fee) { const size_t estimated_tx_weight = estimate_tx_weight(use_rct, n_inputs, mixin, n_outputs, extra_size, bulletproof, clsag, bulletproof_plus); - return calculate_fee_from_weight(base_fee, estimated_tx_weight, fee_multiplier, fee_quantization_mask); + return calculate_fee_from_weight(base_fee, estimated_tx_weight, fee_quantization_mask); } else { const size_t estimated_tx_size = estimate_tx_size(use_rct, n_inputs, mixin, n_outputs, extra_size, bulletproof, clsag, bulletproof_plus); - return calculate_fee(base_fee, estimated_tx_size, fee_multiplier); + return calculate_fee(base_fee, estimated_tx_size); } } @@ -7315,6 +7315,40 @@ uint64_t wallet2::get_base_fee() return get_dynamic_base_fee_estimate(); } //---------------------------------------------------------------------------------------------------- +uint64_t wallet2::get_base_fee(uint32_t priority) +{ + const bool use_2021_scaling = use_fork_rules(HF_VERSION_2021_SCALING, -30 * 1); + if (use_2021_scaling) + { + // clamp and map to 0..3 indices, mapping 0 (default, but should not end up here) to 0, and 1..4 to 0..3 + if (priority == 0) + priority = 1; + else if (priority > 4) + priority = 4; + --priority; + + std::vector fees; + boost::optional result = m_node_rpc_proxy.get_dynamic_base_fee_estimate_2021_scaling(FEE_ESTIMATE_GRACE_BLOCKS, fees); + if (result) + { + MERROR("Failed to determine base fee, using default"); + return FEE_PER_BYTE; + } + if (priority >= fees.size()) + { + MERROR("Failed to determine base fee for priority " << priority << ", using default"); + return FEE_PER_BYTE; + } + return fees[priority]; + } + else + { + const uint64_t base_fee = get_base_fee(); + const uint64_t fee_multiplier = get_fee_multiplier(priority); + return base_fee * fee_multiplier; + } +} +//---------------------------------------------------------------------------------------------------- uint64_t wallet2::get_fee_quantization_mask() { if(m_light_wallet) @@ -7389,9 +7423,8 @@ uint32_t wallet2::adjust_priority(uint32_t priority) { // check if there's a backlog in the tx pool const bool use_per_byte_fee = use_fork_rules(HF_VERSION_PER_BYTE_FEE, 0); - const uint64_t base_fee = get_base_fee(); - const uint64_t fee_multiplier = get_fee_multiplier(1); - const double fee_level = fee_multiplier * base_fee * (use_per_byte_fee ? 1 : (12/(double)13 / (double)1024)); + const uint64_t base_fee = get_base_fee(1); + const double fee_level = base_fee * (use_per_byte_fee ? 1 : (12/(double)13 / (double)1024)); const std::vector> blocks = estimate_backlog({std::make_pair(fee_level, fee_level)}); if (blocks.size() != 1) { @@ -9665,8 +9698,7 @@ std::vector wallet2::create_transactions_2(std::vector wallet2::create_transactions_2(std::vector wallet2::create_transactions_2(std::vector tx_weight_two_rings, error::wallet_internal_error, "Estimated tx weight with 1 input is larger than with 2 inputs!"); const size_t tx_weight_per_ring = tx_weight_two_rings - tx_weight_one_ring; - const uint64_t fractional_threshold = (fee_multiplier * base_fee * tx_weight_per_ring) / (use_per_byte_fee ? 1 : 1024); + const uint64_t fractional_threshold = (base_fee * tx_weight_per_ring) / (use_per_byte_fee ? 1 : 1024); // gather all dust and non-dust outputs belonging to specified subaddresses size_t num_nondust_outputs = 0; @@ -9814,7 +9846,7 @@ std::vector wallet2::create_transactions_2(std::vector wallet2::create_transactions_2(std::vector wallet2::create_transactions_2(std::vector wallet2::create_transactions_2(std::vector wallet2::create_transactions_all(uint64_t below const bool bulletproof = use_fork_rules(get_bulletproof_fork(), 0); const bool bulletproof_plus = use_fork_rules(get_bulletproof_plus_fork(), 0); const bool clsag = use_fork_rules(get_clsag_fork(), 0); - const uint64_t base_fee = get_base_fee(); - const uint64_t fee_multiplier = get_fee_multiplier(priority, get_fee_algorithm()); + const uint64_t base_fee = get_base_fee(priority); const size_t tx_weight_one_ring = estimate_tx_weight(use_rct, 1, fake_outs_count, 2, 0, bulletproof, clsag, bulletproof_plus); const size_t tx_weight_two_rings = estimate_tx_weight(use_rct, 2, fake_outs_count, 2, 0, bulletproof, clsag, bulletproof_plus); THROW_WALLET_EXCEPTION_IF(tx_weight_one_ring > tx_weight_two_rings, error::wallet_internal_error, "Estimated tx weight with 1 input is larger than with 2 inputs!"); const size_t tx_weight_per_ring = tx_weight_two_rings - tx_weight_one_ring; - const uint64_t fractional_threshold = (fee_multiplier * base_fee * tx_weight_per_ring) / (use_per_byte_fee ? 1 : 1024); + const uint64_t fractional_threshold = (base_fee * tx_weight_per_ring) / (use_per_byte_fee ? 1 : 1024); THROW_WALLET_EXCEPTION_IF(unlocked_balance(subaddr_account, false) == 0, error::wallet_internal_error, "No unlocked balance in the specified account"); @@ -10371,8 +10402,7 @@ std::vector wallet2::create_transactions_from(const crypton rct::RangeProofPaddedBulletproof, bulletproof_plus ? 4 : 3 }; - const uint64_t base_fee = get_base_fee(); - const uint64_t fee_multiplier = get_fee_multiplier(priority, get_fee_algorithm()); + const uint64_t base_fee = get_base_fee(priority); const uint64_t fee_quantization_mask = get_fee_quantization_mask(); LOG_PRINT_L2("Starting with " << unused_transfers_indices.size() << " non-dust outputs and " << unused_dust_indices.size() << " dust outputs"); @@ -10399,11 +10429,11 @@ std::vector wallet2::create_transactions_from(const crypton if (use_fork_rules(HF_VERSION_PER_BYTE_FEE)) { const uint64_t estimated_tx_weight_with_one_extra_output = estimate_tx_weight(use_rct, tx.selected_transfers.size() + 1, fake_outs_count, tx.dsts.size()+1, extra.size(), bulletproof, clsag, bulletproof_plus); - fee_dust_threshold = calculate_fee_from_weight(base_fee, estimated_tx_weight_with_one_extra_output, fee_multiplier, fee_quantization_mask); + fee_dust_threshold = calculate_fee_from_weight(base_fee, estimated_tx_weight_with_one_extra_output, fee_quantization_mask); } else { - fee_dust_threshold = base_fee * fee_multiplier * (upper_transaction_weight_limit + 1023) / 1024; + fee_dust_threshold = base_fee * (upper_transaction_weight_limit + 1023) / 1024; } size_t idx = @@ -10437,7 +10467,7 @@ std::vector wallet2::create_transactions_from(const crypton pending_tx test_ptx; const size_t num_outputs = get_num_outputs(tx.dsts, m_transfers, tx.selected_transfers); - needed_fee = estimate_fee(use_per_byte_fee, use_rct, tx.selected_transfers.size(), fake_outs_count, num_outputs, extra.size(), bulletproof, clsag, bulletproof_plus, base_fee, fee_multiplier, fee_quantization_mask); + needed_fee = estimate_fee(use_per_byte_fee, use_rct, tx.selected_transfers.size(), fake_outs_count, num_outputs, extra.size(), bulletproof, clsag, bulletproof_plus, base_fee, fee_quantization_mask); // add N - 1 outputs for correct initial fee estimation for (size_t i = 0; i < ((outputs > 1) ? outputs - 1 : outputs); ++i) @@ -10452,7 +10482,7 @@ std::vector wallet2::create_transactions_from(const crypton transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, 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); - needed_fee = calculate_fee(use_per_byte_fee, test_ptx.tx, txBlob.size(), base_fee, fee_multiplier, fee_quantization_mask); + needed_fee = calculate_fee(use_per_byte_fee, test_ptx.tx, txBlob.size(), base_fee, fee_quantization_mask); available_for_fee = test_ptx.fee + test_ptx.change_dts.amount; for (auto &dt: test_ptx.dests) available_for_fee += dt.amount; @@ -10489,7 +10519,7 @@ std::vector wallet2::create_transactions_from(const crypton transfer_selected(tx.dsts, tx.selected_transfers, fake_outs_count, outs, unlock_time, needed_fee, extra, detail::digit_split_strategy, tx_dust_policy(::config::DEFAULT_DUST_THRESHOLD), test_tx, test_ptx); txBlob = t_serializable_object_to_blob(test_ptx.tx); - needed_fee = calculate_fee(use_per_byte_fee, test_ptx.tx, txBlob.size(), base_fee, fee_multiplier, fee_quantization_mask); + needed_fee = calculate_fee(use_per_byte_fee, test_ptx.tx, txBlob.size(), base_fee, fee_quantization_mask); LOG_PRINT_L2("Made an attempt at a final " << get_weight_string(test_ptx.tx, txBlob.size()) << " tx, with " << print_money(test_ptx.fee) << " fee and " << print_money(test_ptx.change_dts.amount) << " change"); } while (needed_fee > test_ptx.fee); @@ -10816,7 +10846,7 @@ std::vector wallet2::create_unmixable_sweep_transactions() const bool hf1_rules = use_fork_rules(2, 10); // first hard fork has version 2 tx_dust_policy dust_policy(hf1_rules ? 0 : ::config::DEFAULT_DUST_THRESHOLD); - const uint64_t base_fee = get_base_fee(); + const uint64_t base_fee = get_base_fee(1); // may throw std::vector unmixable_outputs = select_available_unmixable_outputs(); diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index f5da348a9..59f1ca94a 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -1427,8 +1427,9 @@ private: std::vector> estimate_backlog(const std::vector> &fee_levels); std::vector> estimate_backlog(uint64_t min_tx_weight, uint64_t max_tx_weight, const std::vector &fees); - uint64_t estimate_fee(bool use_per_byte_fee, bool use_rct, int n_inputs, int mixin, int n_outputs, size_t extra_size, bool bulletproof, bool clsag, bool bulletproof_plus, uint64_t base_fee, uint64_t fee_multiplier, uint64_t fee_quantization_mask) const; + uint64_t estimate_fee(bool use_per_byte_fee, bool use_rct, int n_inputs, int mixin, int n_outputs, size_t extra_size, bool bulletproof, bool clsag, bool bulletproof_plus, uint64_t base_fee, uint64_t fee_quantization_mask) const; uint64_t get_fee_multiplier(uint32_t priority, int fee_algorithm = -1); + uint64_t get_base_fee(uint32_t priority); uint64_t get_base_fee(); uint64_t get_fee_quantization_mask(); uint64_t get_min_ring_size(); diff --git a/tests/functional_tests/blockchain.py b/tests/functional_tests/blockchain.py index 4c8f367c0..328c225ff 100755 --- a/tests/functional_tests/blockchain.py +++ b/tests/functional_tests/blockchain.py @@ -81,10 +81,10 @@ class BlockchainTest(): assert ok res = daemon.get_fee_estimate() - assert res.fee == 234562 + assert res.fee == 1200000 assert res.quantization_mask == 10000 res = daemon.get_fee_estimate(10) - assert res.fee <= 234562 + assert res.fee <= 1200000 # generate blocks res_generateblocks = daemon.generateblocks('42ey1afDFnn4886T7196doS9GPMzexD9gXpsZJDwVjeRVdFCSoHnv7KPbBeGpzJBzHRCAs9UxqeoyFQMYbqSWYTfJJQAWDm', blocks) @@ -243,10 +243,10 @@ class BlockchainTest(): assert res.histogram[i].recent_instances == 0 res = daemon.get_fee_estimate() - assert res.fee == 234560 + assert res.fee == 1200000 assert res.quantization_mask == 10000 res = daemon.get_fee_estimate(10) - assert res.fee <= 234560 + assert res.fee <= 1200000 def _test_alt_chains(self): print('Testing alt chains') diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index b5ee7af01..55818dc93 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -77,6 +77,7 @@ set(unit_tests_sources pruning.cpp random.cpp rolling_median.cpp + scaling_2021.cpp serialization.cpp sha256.cpp slow_memmem.cpp diff --git a/tests/unit_tests/scaling_2021.cpp b/tests/unit_tests/scaling_2021.cpp new file mode 100644 index 000000000..9e8d15544 --- /dev/null +++ b/tests/unit_tests/scaling_2021.cpp @@ -0,0 +1,187 @@ +// Copyright (c) 2019-2020, The Monero 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. + +// References: +// - https://github.com/ArticMine/Monero-Documents/blob/master/MoneroScaling2021.pdf +// - https://github.com/monero-project/research-lab/issues/70 + +#define IN_UNIT_TESTS + +#include "gtest/gtest.h" +#include "cryptonote_core/blockchain.h" +#include "cryptonote_core/tx_pool.h" +#include "cryptonote_core/cryptonote_core.h" +#include "blockchain_db/testdb.h" + +namespace +{ + +class TestDB: public cryptonote::BaseTestDB +{ +public: + TestDB() { m_open = true; } +}; + +} + +#define PREFIX_WINDOW(hf_version,window) \ + std::unique_ptr bc; \ + cryptonote::tx_memory_pool txpool(*bc); \ + bc.reset(new cryptonote::Blockchain(txpool)); \ + struct get_test_options { \ + const std::pair hard_forks[3]; \ + const cryptonote::test_options test_options = { \ + hard_forks, \ + window, \ + }; \ + get_test_options(): hard_forks{std::make_pair(1, (uint64_t)0), std::make_pair((uint8_t)hf_version, (uint64_t)1), std::make_pair((uint8_t)0, (uint64_t)0)} {} \ + } opts; \ + cryptonote::Blockchain *blockchain = bc.get(); \ + bool r = blockchain->init(new TestDB(), cryptonote::FAKECHAIN, true, &opts.test_options, 0, NULL); \ + ASSERT_TRUE(r) + +#define PREFIX(hf_version) PREFIX_WINDOW(hf_version, TEST_LONG_TERM_BLOCK_WEIGHT_WINDOW) + +TEST(fee_2021_scaling, relay_fee_cases_from_pdf) +{ + PREFIX_WINDOW(HF_VERSION_2021_SCALING, CRYPTONOTE_LONG_TERM_BLOCK_WEIGHT_WINDOW_SIZE); + + ASSERT_EQ(bc->get_dynamic_base_fee(1200000000000, 300000, HF_VERSION_2021_SCALING-1), 8000); + ASSERT_EQ(bc->get_dynamic_base_fee(1200000000000, 300000, HF_VERSION_2021_SCALING), 38000); + ASSERT_EQ(bc->get_dynamic_base_fee(1200000000000, 1425000, HF_VERSION_2021_SCALING-1), 1684 /*1680*/); + ASSERT_EQ(bc->get_dynamic_base_fee(1200000000000, 1425000, HF_VERSION_2021_SCALING), 1684 /*1680*/); + ASSERT_EQ(bc->get_dynamic_base_fee(1200000000000, 1500000, HF_VERSION_2021_SCALING-1), 1600); + ASSERT_EQ(bc->get_dynamic_base_fee(1200000000000, 1500000, HF_VERSION_2021_SCALING), 1520); + + ASSERT_EQ(bc->get_dynamic_base_fee(600000000000, 300000, HF_VERSION_2021_SCALING-1), 4000); + ASSERT_EQ(bc->get_dynamic_base_fee(600000000000, 300000, HF_VERSION_2021_SCALING), 19000); + ASSERT_EQ(bc->get_dynamic_base_fee(600000000000, 1425000, HF_VERSION_2021_SCALING-1), 842 /*840*/); + ASSERT_EQ(bc->get_dynamic_base_fee(600000000000, 1425000, HF_VERSION_2021_SCALING), 842 /*840*/); + ASSERT_EQ(bc->get_dynamic_base_fee(600000000000, 1500000, HF_VERSION_2021_SCALING-1), 800); + ASSERT_EQ(bc->get_dynamic_base_fee(600000000000, 1500000, HF_VERSION_2021_SCALING), 760); +} + +TEST(fee_2021_scaling, wallet_fee_cases_from_pdf) +{ + PREFIX_WINDOW(HF_VERSION_2021_SCALING, CRYPTONOTE_LONG_TERM_BLOCK_WEIGHT_WINDOW_SIZE); + std::vector fees; + + fees.clear(); + bc->get_dynamic_base_fee_estimate_2021_scaling(10, 600000000000, 300000, 300000, fees); + ASSERT_EQ(fees.size(), 4); + ASSERT_EQ(fees[0], 20000); + ASSERT_EQ(fees[1], 80000); + ASSERT_EQ(fees[2], 320000); + ASSERT_EQ(fees[3], 4000000); + + fees.clear(); + bc->get_dynamic_base_fee_estimate_2021_scaling(10, 600000000000, 15000000, 300000, fees); + ASSERT_EQ(fees.size(), 4); + ASSERT_EQ(fees[0], 20000); + ASSERT_EQ(fees[1], 80000); + ASSERT_EQ(fees[2], 320000); + ASSERT_EQ(fees[3], 1300000); + + fees.clear(); + bc->get_dynamic_base_fee_estimate_2021_scaling(10, 600000000000, 1425000, 1425000, fees); + ASSERT_EQ(fees.size(), 4); + ASSERT_EQ(fees[0], 890); + ASSERT_EQ(fees[1], 3600); + ASSERT_EQ(fees[2], 68000); + ASSERT_EQ(fees[3], 850000 /* 842000 */); + + fees.clear(); + bc->get_dynamic_base_fee_estimate_2021_scaling(10, 600000000000, 1500000, 1500000, fees); + ASSERT_EQ(fees.size(), 4); + ASSERT_EQ(fees[0], 800); + ASSERT_EQ(fees[1], 3200); + ASSERT_EQ(fees[2], 64000); + ASSERT_EQ(fees[3], 800000); + + fees.clear(); + bc->get_dynamic_base_fee_estimate_2021_scaling(10, 600000000000, 75000000, 1500000, fees); + ASSERT_EQ(fees.size(), 4); + ASSERT_EQ(fees[0], 800); + ASSERT_EQ(fees[1], 3200); + ASSERT_EQ(fees[2], 64000); + ASSERT_EQ(fees[3], 260000); +} + +TEST(fee_2021_scaling, rounding) +{ + ASSERT_EQ(cryptonote::round_money_up("27810", 3), "27900.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("37.94", 3), "38.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("0.5555", 3), "0.556000000000"); + ASSERT_EQ(cryptonote::round_money_up("0.002342", 3), "0.002350000000"); + + ASSERT_EQ(cryptonote::round_money_up("27810", 2), "28000.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("37.94", 2), "38.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("0.5555", 2), "0.560000000000"); + ASSERT_EQ(cryptonote::round_money_up("0.002342", 2), "0.002400000000"); + + ASSERT_EQ(cryptonote::round_money_up("0", 8), "0.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("0.0", 8), "0.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("50.0", 8), "50.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("0.002342", 8), "0.002342000000"); + ASSERT_EQ(cryptonote::round_money_up("0.002342", 1), "0.003000000000"); + ASSERT_EQ(cryptonote::round_money_up("12345", 8), "12345.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("45678", 1), "50000.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.234", 1), "2.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.0000001", 4), "1.001000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.0020001", 4), "1.003000000000"); + + ASSERT_EQ(cryptonote::round_money_up("1.999999", 1), "2.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 2), "2.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 3), "2.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 4), "2.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 5), "2.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 6), "2.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 7), "1.999999000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 8), "1.999999000000"); + ASSERT_EQ(cryptonote::round_money_up("1.999999", 9), "1.999999000000"); + + ASSERT_EQ(cryptonote::round_money_up("2.000001", 1), "3.000000000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 2), "2.100000000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 3), "2.010000000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 4), "2.001000000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 5), "2.000100000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 6), "2.000010000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 7), "2.000001000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 8), "2.000001000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 9), "2.000001000000"); + ASSERT_EQ(cryptonote::round_money_up("2.000001", 4000), "2.000001000000"); + + ASSERT_EQ(cryptonote::round_money_up("999", 2), "1000.000000000000"); + + ASSERT_THROW(cryptonote::round_money_up("1.23", 0), std::runtime_error); + ASSERT_THROW(cryptonote::round_money_up("18446744.073709551615", 1), std::runtime_error); + ASSERT_THROW(cryptonote::round_money_up("18446744.073709551615", 2), std::runtime_error); + ASSERT_THROW(cryptonote::round_money_up("18446744.073709551615", 12), std::runtime_error); + ASSERT_THROW(cryptonote::round_money_up("18446744.073709551615", 19), std::runtime_error); + ASSERT_EQ(cryptonote::round_money_up("18446744.073709551615", 20), "18446744.073709551615"); +}