diff --git a/src/carrot_impl/carrot_tx_builder_types.h b/src/carrot_impl/carrot_tx_builder_types.h index 53a21b343..23e393cfe 100644 --- a/src/carrot_impl/carrot_tx_builder_types.h +++ b/src/carrot_impl/carrot_tx_builder_types.h @@ -66,7 +66,7 @@ struct CarrotPaymentProposalVerifiableSelfSendV1 }; using select_inputs_func_t = std::function&, // absolute fee per input count const std::size_t, // number of normal payment proposals const std::size_t, // number of self-send payment proposals @@ -74,7 +74,7 @@ using select_inputs_func_t = std::function; using carve_fees_and_balance_func_t = std::function&, // normal payment proposals [inout] std::vector& // selfsend payment proposals [inout] diff --git a/src/carrot_impl/carrot_tx_builder_utils.cpp b/src/carrot_impl/carrot_tx_builder_utils.cpp index 848a18a66..948972b94 100644 --- a/src/carrot_impl/carrot_tx_builder_utils.cpp +++ b/src/carrot_impl/carrot_tx_builder_utils.cpp @@ -193,7 +193,7 @@ void make_carrot_transaction_proposal_v1(const std::vector &normal_payment_proposals, std::vector &selfsend_payment_proposals @@ -313,7 +313,7 @@ void make_carrot_transaction_proposal_v1_transfer( "make unsigned transaction transfer subtractable: bug: added implicit change output has non-zero amount"); // start by setting the last selfsend amount equal to (inputs - outputs), before fee - boost::multiprecision::int128_t implicit_change_amount = input_sum_amount; + boost::multiprecision::uint128_t implicit_change_amount = input_sum_amount; for (const CarrotPaymentProposalV1 &normal_payment_proposal : normal_payment_proposals) implicit_change_amount -= normal_payment_proposal.amount; for (const CarrotPaymentProposalVerifiableSelfSendV1 &selfsend_payment_proposal : selfsend_payment_proposals) @@ -434,7 +434,7 @@ void make_carrot_transaction_proposal_v1_sweep( // define input selection callback, which is just a shuttle for `selected_inputs` select_inputs_func_t select_inputs = [&selected_inputs] ( - const boost::multiprecision::int128_t&, + const boost::multiprecision::uint128_t&, const std::map&, const std::size_t, const std::size_t, @@ -447,7 +447,7 @@ void make_carrot_transaction_proposal_v1_sweep( // define carves fees and balance callback carve_fees_and_balance_func_t carve_fees_and_balance = [is_selfsend_sweep] ( - const boost::multiprecision::int128_t &input_sum_amount, + const boost::multiprecision::uint128_t &input_sum_amount, const rct::xmr_amount fee, std::vector &normal_payment_proposals, std::vector &selfsend_payment_proposals @@ -466,7 +466,7 @@ void make_carrot_transaction_proposal_v1_sweep( std::shuffle(amount_ptrs.begin(), amount_ptrs.end(), crypto::random_device{}); // disburse amount equally amongst modifiable amounts - const boost::multiprecision::int128_t output_sum_amount = input_sum_amount - fee; + const boost::multiprecision::uint128_t output_sum_amount = input_sum_amount - fee; const rct::xmr_amount minimum_sweep_amount = boost::numeric_cast(output_sum_amount / amount_ptrs.size()); const size_t num_remaining = diff --git a/src/carrot_impl/carrot_tx_format_utils.cpp b/src/carrot_impl/carrot_tx_format_utils.cpp index f6e6976a5..edd595918 100644 --- a/src/carrot_impl/carrot_tx_format_utils.cpp +++ b/src/carrot_impl/carrot_tx_format_utils.cpp @@ -31,6 +31,7 @@ //local headers #include "carrot_core/enote_utils.h" +#include "carrot_core/exceptions.h" #include "carrot_core/payment_proposal.h" #include "common/container_helpers.h" #include "cryptonote_basic/cryptonote_format_utils.h" @@ -113,6 +114,7 @@ static bool try_load_carrot_ephemeral_pubkeys_from_extra(const std::vector rhs.core.amount) - return 1; - - // then prefer older - if (lhs.block_index < rhs.block_index) - return 1; - else if (lhs.block_index > rhs.block_index) - return -1; - - // It should be computationally intractable for lhs.is_external != rhs.is_external, but I haven't - // looked into it too deeply. I guess you would want to prefer whichever one !is_external. - - return 0; -} -//------------------------------------------------------------------------------------------------------------------- -//------------------------------------------------------------------------------------------------------------------- static std::set set_union(const std::set &a, const std::set &b) { std::set c = a; @@ -98,10 +75,10 @@ static void stable_sort_indices_by_amount(const epee::span &indices_inout) { std::stable_sort(indices_inout.begin(), indices_inout.end(), - [input_candidates](const size_t a, const size_t b) -> bool + [input_candidates](const std::size_t a, const std::size_t b) -> bool { - CHECK_AND_ASSERT_THROW_MES(a < input_candidates.size() && b < input_candidates.size(), - "input candidate index out of range"); + CARROT_CHECK_AND_THROW(a < input_candidates.size() && b < input_candidates.size(), + std::out_of_range, "input candidate index out of range"); return input_candidates[a].core.amount < input_candidates[b].core.amount; }); } @@ -111,304 +88,436 @@ static void stable_sort_indices_by_block_index(const epee::span &indices_inout) { std::stable_sort(indices_inout.begin(), indices_inout.end(), - [input_candidates](const size_t a, const size_t b) -> bool + [input_candidates](const std::size_t a, const std::size_t b) -> bool { - CHECK_AND_ASSERT_THROW_MES(a < input_candidates.size() && b < input_candidates.size(), - "input candidate index out of range"); + CARROT_CHECK_AND_THROW(a < input_candidates.size() && b < input_candidates.size(), + std::out_of_range, "input candidate index out of range"); return input_candidates[a].block_index < input_candidates[b].block_index; }); } //------------------------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------------------------- -static std::pair input_count_for_max_usable_money( +static std::pair input_count_for_max_usable_money( const epee::span input_candidates, - const std::set &selectable_inputs, - const std::map &fee_by_input_count) + const std::set &selectable_inputs, + std::size_t max_num_input_count, + const std::map &fee_by_input_count) { - // Returns (N, X) where the X is the sum of the amounts of the greatest N <= CARROT_MAX_TX_INPUTS + // Returns (N, X) where the X is the sum of the amounts of the greatest N <= max_num_input_count // inputs from selectable_inputs, maximizing X - F(N). F(N) is the fee for this transaction, // given input count N. This should correctly handle "almost-dust": inputs which are less than // the fee, but greater than or equal to the difference of the fee compared to excluding that // input. If this function returns N == 0, then there aren't enough usable funds, i.e. no N // exists such that X - F(N) > 0. - size_t num_ins = 0; - boost::multiprecision::int128_t cumulative_input_sum = 0; - rct::xmr_amount last_fee = 0; + if (fee_by_input_count.empty() || selectable_inputs.empty()) + return {0, 0}; - std::vector selectable_inputs_vec(selectable_inputs.cbegin(), selectable_inputs.cend()); - stable_sort_indices_by_amount(input_candidates, selectable_inputs_vec); + max_num_input_count = std::min(max_num_input_count, selectable_inputs.size()); + CARROT_CHECK_AND_THROW(max_num_input_count <= fee_by_input_count.crbegin()->first, + too_few_inputs, "fee by input count does not contain info for provided max input count"); - // for selectable indices in descending amount... - for (auto it = selectable_inputs_vec.crbegin(); it != selectable_inputs_vec.crend(); ++it) + // maintain list of top amounts of selectable_inputs + std::multiset top_amounts; + for (const std::size_t selectable_input : selectable_inputs) { - if (num_ins == CARROT_MAX_TX_INPUTS) - break; + CARROT_CHECK_AND_THROW(selectable_input < input_candidates.size(), + std::out_of_range, "selectable input out of range"); + const rct::xmr_amount amount = input_candidates[selectable_input].core.amount; + top_amounts.insert(amount); + if (top_amounts.size() > max_num_input_count) + top_amounts.erase(top_amounts.cbegin()); + } + + // add up all the top amounts from the greatest to least until one fails to pay for its own marginal fee + std::size_t num_ins = 0; + rct::xmr_amount last_fee = 0; + boost::multiprecision::uint128_t cumulative_input_sum = 0; + for (auto amount_it = top_amounts.crbegin(); amount_it != top_amounts.crend(); ++amount_it) + { + const rct::xmr_amount amount = *amount_it; + const rct::xmr_amount current_fee = fee_by_input_count.at(num_ins + 1); + CARROT_CHECK_AND_THROW(current_fee > last_fee, + carrot_logic_error, "provided fee by input count is not monotonically increasing"); + const rct::xmr_amount marginal_fee_diff = current_fee - last_fee; + if (amount <= marginal_fee_diff) + break; ++num_ins; - - const rct::xmr_amount amount = input_candidates[*it].core.amount; - - if (amount < fee_by_input_count.at(num_ins) - last_fee) - { - // then this input doesn't pay for itself, rollback previous state and break - // since all next inputs will have same amount or less - --num_ins; - break; - } - + last_fee = current_fee; cumulative_input_sum += amount; - last_fee = fee_by_input_count.at(num_ins); } return {num_ins, cumulative_input_sum}; } //------------------------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------------------------- +int compare_input_candidate_same_ki(const CarrotPreSelectedInput &lhs, const CarrotPreSelectedInput &rhs) +{ + CARROT_CHECK_AND_THROW(lhs.core.key_image == rhs.core.key_image, + component_out_of_order, "this function is not meant to compare inputs of different key images"); + + // First prefer the higher amount, + if (lhs.core.amount < rhs.core.amount) + return -1; + else if (lhs.core.amount > rhs.core.amount) + return 1; + + // Then prefer older, + if (lhs.block_index < rhs.block_index) + return 1; + else if (lhs.block_index > rhs.block_index) + return -1; + + // Then prefer Carrot over pre-Carrot. It should be computationally intractable for + // lhs.is_pre_carrot != rhs.is_pre_carrot, when they both successfully scan, but I haven't + // looked into it too deeply. + if (lhs.is_pre_carrot && !rhs.is_pre_carrot) + return -1; + else if (!lhs.is_pre_carrot && rhs.is_pre_carrot) + return 1; + + // Then prefer internal over external. Same tractability note as with is_pre_carrot. + if (lhs.is_external && !rhs.is_external) + return -1; + else if (!lhs.is_external && rhs.is_external) + return 1; + + return 0; +} +//------------------------------------------------------------------------------------------------------------------- +std::vector> form_preferred_input_candidate_subsets( + const epee::span input_candidates, + const std::uint32_t flags, + const bool is_normal_transfer) +{ + using namespace InputSelectionFlags; + + // Sanity check flags + const bool confused_qfs = (flags & ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS) && + !(flags & ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS); + CARROT_CHECK_AND_THROW(!confused_qfs, std::invalid_argument, + "It does not make sense to allow pre-carrot inputs in normal transfers, but not external carrot inputs."); + + // 1. Compile map of best input candidates by key image to mitigate the "burning bug" for legacy enotes + std::unordered_map best_input_by_key_image; + for (size_t i = 0; i < input_candidates.size(); ++i) + { + const CarrotPreSelectedInput &input_candidate = input_candidates[i]; + auto it = best_input_by_key_image.find(input_candidate.core.key_image); + if (it == best_input_by_key_image.end()) + { + best_input_by_key_image[input_candidate.core.key_image] = i; + } + else + { + const CarrotPreSelectedInput &other_input_candidate = input_candidates[it->second]; + if (compare_input_candidate_same_ki(other_input_candidate, input_candidate) < 0) + it->second = i; + } + } + + // 2. Collect set of non-burned inputs + std::set all_non_burned_inputs; + for (const auto &best_input : best_input_by_key_image) + all_non_burned_inputs.insert(best_input.second); + + // 3. Partition into: + // a) Pre-carrot (no quantum forward secrecy) + // b) External carrot (quantum forward secret if public address not known) + // c) Internal carrot (always quantum forward secret unless secret keys known) + std::set pre_carrot_inputs; + std::set external_carrot_inputs; + std::set internal_inputs; + for (std::size_t candidate_idx : all_non_burned_inputs) + { + if (input_candidates[candidate_idx].is_pre_carrot) + pre_carrot_inputs.insert(candidate_idx); + else if (input_candidates[candidate_idx].is_external) + external_carrot_inputs.insert(candidate_idx); + else + internal_inputs.insert(candidate_idx); + } + + // 4. Calculate misc features + const bool must_use_internal = !(flags & ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS) && is_normal_transfer; + const bool allow_mixed_externality = (flags & ALLOW_MIXED_INTERNAL_EXTERNAL) && !must_use_internal; + const bool must_use_carrot = !(flags & ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS) && is_normal_transfer; + const bool allow_mixed_carrotness = (flags & ALLOW_MIXED_CARROT_PRE_CARROT) && !must_use_carrot; + + // 5. We should prefer to spend non-forward-secret enotes in transactions where all the outputs + // are going back to ourself. Otherwise, if we spend these enotes while transferring money to + // another entity, an external observer who A) has a quantum computer, and B) knows one of their + // public addresses, will be able to trace the money transfer. Such an observer will always be + // able to tell which view-incoming keys / accounts these non-forward-secrets enotes belong to, + // their amounts, and where they're spent. So since they already know that information, churning + // back to oneself doesn't actually reveal that much more additional information. + const bool prefer_non_fs = !is_normal_transfer; + CARROT_CHECK_AND_THROW(!must_use_internal || !prefer_non_fs, + carrot_logic_error, "bug: must_use_internal AND prefer_non_fs are true"); + + // There is no "prefer pre-carrot" variable since in the case that we prefer spending + // non-forward-secret, we always prefer first spending pre-carrot over carrot, if it is allowed + + // 6. Define input_candidate_subsets and how to add to it + std::vector> input_candidate_subsets; + input_candidate_subsets.reserve(8); + const auto push_subset = [&input_candidate_subsets](const std::set &subset) + { + if (subset.empty()) return; + const auto subset_it = std::find(input_candidate_subsets.cbegin(), input_candidate_subsets.cend(), subset); + if (subset_it != input_candidate_subsets.cend()) return; // subset already present (could be more efficient) + input_candidate_subsets.push_back(subset); + }; + + // 7. Try dispatching for non-forward-secret input subsets, if preferred in this context + if (prefer_non_fs) + { + // try getting rid of pre-carrot enotes first, if allowed + if (!must_use_carrot) + push_subset(pre_carrot_inputs); + + // ... then external carrot + push_subset(external_carrot_inputs); + } + + // 8. Try dispatching for internal + push_subset(internal_inputs); + + // 9. Try dispatching for non-FS *after* internal, if allowed and not already tried + if (!must_use_internal || !prefer_non_fs) + { + // Spending non-FS inputs in a normal transfer transaction is not ideal, but at least + // when partition it like this, we aren't "dirtying" the carrot with the pre-carrot, and + // the internal with the external + if (!must_use_carrot) + push_subset(pre_carrot_inputs); + push_subset(external_carrot_inputs); + } + + // 10. Try dispatching for all non-FS (mixed pre-carrot & carrot external), if allowed + if (allow_mixed_carrotness) + { + // We're mixing carrot/pre-carrot spends here, but avoiding "dirtying" the internal + push_subset(set_union(pre_carrot_inputs, external_carrot_inputs)); + } + + // 11. Try dispatching for all carrot, if allowed + if (allow_mixed_externality) + { + // We're mixing internal & external carrot spends here, but avoiding "dirtying" the + // carrot spends with pre-carrot spends. This will be quantum forward secret iff the + // adversary doesn't know one of your public addresses + push_subset(set_union(external_carrot_inputs, internal_inputs)); + } + + //! @TODO: MRL discussion about whether step 11 or step 12 should go first. In other words, + // do we prefer to avoid dirtying internal, and protect against quantum adversaries + // who know your public addresses? Or do we prefer to avoid dirtying w/ pre-carrot, + // and protect against quantum adversaries with no special knowledge of your public + // addresses, but whose attacks are only relevant when spending pre-FCMP++ enotes? + + // 12. Try dispatching for everything, if allowed + if (allow_mixed_carrotness && allow_mixed_externality) + push_subset(all_non_burned_inputs); + + // Notice that we don't combine just the pre_carrot_inputs and internal_inputs by themselves + + return input_candidate_subsets; +} +//------------------------------------------------------------------------------------------------------------------- +std::vector get_input_counts_in_preferred_order() +{ + // 1 or 2 randomly, then + // other ascending powers of 2, then + // other ascending positive numbers + + //! @TODO: MRL discussion about 2 vs 1 default input count when 1 input can pay. If we default + // to 1, then that may reveal more information about the amount, and reveals that one can't pay + // with 1 output when using 2. Vice versa, if we default to 2, then that means that one only + // owns 1 output when using 1. It may be the most advantageous to randomly switch between + // preferring 1 vs 2. See: https://lavalle.pl/planning/node437.html. Con to this approach: if we + // default to 1 over 2 always then there's scenarios where we net save tx fees and proving time. + + static_assert(CARROT_MAX_TX_INPUTS == FCMP_PLUS_PLUS_MAX_INPUTS, "inconsistent input count max limit"); + static_assert(CARROT_MIN_TX_INPUTS == 1 && CARROT_MAX_TX_INPUTS == 8, + "refactor this function for different input count limits"); + + const bool random_bit = 0 == (crypto::rand() & 0x01); + if (random_bit) + return {2, 1, 4, 8, 3, 5, 6, 7}; + else + return {1, 2, 4, 8, 3, 5, 6, 7}; +} +//------------------------------------------------------------------------------------------------------------------- select_inputs_func_t make_single_transfer_input_selector( const epee::span input_candidates, const epee::span policies, const std::uint32_t flags, std::set *selected_input_indices_out) { - using namespace InputSelectionFlags; - - CHECK_AND_ASSERT_THROW_MES(!policies.empty(), - "make_single_transfer_input_selector: no input selection policies provided"); - - // Sanity check flags - const bool confused_qfs = (flags & ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS) && - !(flags & ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS); - CHECK_AND_ASSERT_THROW_MES(!confused_qfs, - "make single transfer input selector: It does not make sense to allow pre-carrot inputs in normal transfers, " - "but not external carrot inputs."); - - // input selector :) - return [=](const boost::multiprecision::int128_t &nominal_output_sum, + // input selector :D + return [=](const boost::multiprecision::uint128_t &nominal_output_sum, const std::map &fee_by_input_count, const std::size_t num_normal_payment_proposals, const std::size_t num_selfsend_payment_proposals, std::vector &selected_inputs_out) { - CHECK_AND_ASSERT_THROW_MES(!fee_by_input_count.empty(), - "make_single_transfer_input_selector: no provided allowed input count"); + using namespace InputSelectionFlags; + // 1. Sanity checks valid arguments + const std::size_t n_candidates = input_candidates.size(); + CARROT_CHECK_AND_THROW(!fee_by_input_count.empty(), missing_components, "no provided allowed input count"); + CARROT_CHECK_AND_THROW(!policies.empty(), missing_components, "no input selection policies provided"); + CARROT_CHECK_AND_THROW(n_candidates, not_enough_money, "no input candidates provided"); + + // 2. Log MDEBUG("Running single transfer input selector with " << input_candidates.size() << " candidates and " << policies.size() << " policies, for " << num_normal_payment_proposals << " normal payment proposals, " << num_selfsend_payment_proposals << " self-send payment proposals, " - << cryptonote::print_money(boost::numeric_cast(nominal_output_sum)) + << cryptonote::print_money(nominal_output_sum) << " output sum, and fee range " << cryptonote::print_money(fee_by_input_count.cbegin()->second) << "-" << cryptonote::print_money(fee_by_input_count.crbegin()->second)); - // 1. Compile map of best input candidates by key image to mitigate the "burning bug" for legacy enotes - std::unordered_map best_input_by_key_image; - for (size_t i = 0; i < input_candidates.size(); ++i) - { - const CarrotPreSelectedInput &input_candidate = input_candidates[i]; - auto it = best_input_by_key_image.find(input_candidate.core.key_image); - if (it == best_input_by_key_image.end()) - { - best_input_by_key_image[input_candidate.core.key_image] = i; - } - else - { - const CarrotPreSelectedInput &other_input_candidate = input_candidates[it->second]; - if (compare_input_candidate_same_ki(other_input_candidate, input_candidate) < 0) - it->second = i; - } - } - - // 2. Collect set of non-burned inputs - std::set all_non_burned_inputs; - for (const auto &best_input : best_input_by_key_image) - all_non_burned_inputs.insert(best_input.second); - - // 3. Partition into: - // a) Pre-carrot (no quantum forward secrecy) - // b) External carrot (quantum forward secret if public address not known) - // c) Internal carrot (always quantum forward secret unless secret keys known) - std::set pre_carrot_inputs; - std::set external_carrot_inputs; - std::set internal_inputs; - for (size_t candidate_idx : all_non_burned_inputs) - { - if (input_candidates[candidate_idx].is_pre_carrot) - pre_carrot_inputs.insert(candidate_idx); - else if (input_candidates[candidate_idx].is_external) - external_carrot_inputs.insert(candidate_idx); - else - internal_inputs.insert(candidate_idx); - } - - // 4. Calculate minimum required input money sum for a given input count + // 3. Calculate minimum required input money sum for a given input count const bool subtract_fee = flags & IS_KNOWN_FEE_SUBTRACTABLE; - std::map required_money_by_input_count; + std::map required_money_by_input_count; for (const auto &fee_and_input_count : fee_by_input_count) { required_money_by_input_count[fee_and_input_count.first] = nominal_output_sum + (subtract_fee ? 0 : fee_and_input_count.second); } + const boost::multiprecision::uint128_t absolute_minimum_required_money + = required_money_by_input_count.cbegin()->second; - // 5. Calculate misc features - const bool must_use_internal = !(flags & ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS) && - (num_normal_payment_proposals != 0); - const bool allow_mixed_externality = (flags & ALLOW_MIXED_INTERNAL_EXTERNAL) && - !must_use_internal; - const bool must_use_carrot = !(flags & ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS) && - (num_normal_payment_proposals != 0); - const bool allow_mixed_carrotness = (flags & ALLOW_MIXED_CARROT_PRE_CARROT) && - !must_use_carrot; + // 4. Quick check of total money and single tx input count limited total money + boost::multiprecision::uint128_t total_candidate_money = 0; + for (const CarrotPreSelectedInput &input_candidate : input_candidates) + total_candidate_money += input_candidate.core.amount; + CARROT_CHECK_AND_THROW(total_candidate_money >= absolute_minimum_required_money, + not_enough_money, + "Not enough money in all inputs (" << cryptonote::print_money(total_candidate_money) + << ") to fund minimum output sum (" << cryptonote::print_money(absolute_minimum_required_money) << ')'); - // We should prefer to spend non-forward-secret enotes in transactions where all the outputs are going back to - // ourself. Otherwise, if we spend these enotes while transferring money to another entity, an external observer - // who A) has a quantum computer, and B) knows one of their public addresses, will be able to trace the money - // transfer. Such an observer will always be able to tell which view-incoming keys / accounts these - // non-forward-secrets enotes belong to, their amounts, and where they're spent. So since they already know that - // information, churning back to oneself doesn't actually reveal that much more additional information. - const bool prefer_non_fs = num_normal_payment_proposals == 0; - CHECK_AND_ASSERT_THROW_MES(!must_use_internal || !prefer_non_fs, - "make_single_transfer_input_selector: bug: must_use_internal AND prefer_non_fs are true"); + std::set all_idxs; for (std::size_t i = 0; i < input_candidates.size(); ++i) all_idxs.insert(i); + const std::pair max_usable_money = + input_count_for_max_usable_money(input_candidates, all_idxs, FCMP_PLUS_PLUS_MAX_INPUTS, fee_by_input_count); + CARROT_CHECK_AND_THROW(max_usable_money.second >= absolute_minimum_required_money, + not_enough_usable_money, + "Not enough usable money in top " << max_usable_money.first << " inputs (" + << cryptonote::print_money(max_usable_money.second) << ") to fund minimum output sum (" + << cryptonote::print_money(absolute_minimum_required_money) << ')'); - // There is no "prefer pre-carrot" variable since in the case that we prefer spending non-forward-secret, we - // always prefer first spending pre-carrot over carrot, if it is allowed + // 5. Get preferred input candidate subsets + //! @TODO: dummy check num_normal_payment_proposals + const std::vector> input_candidate_subsets = form_preferred_input_candidate_subsets( + input_candidates, + flags, + num_normal_payment_proposals); - // 6. Short-hand functor for dispatching input selection on a subset of inputs - // Note: Result goes into `selected_inputs_indices`. If already populated, then this functor does nothing + // 6. Get preferred transaction input counts + const std::vector input_counts = get_input_counts_in_preferred_order(); + + // 7. For each input candidate subset... std::set selected_inputs_indices; - const auto try_dispatch_input_selection = - [&](const std::set &selectable_indices) + for (const std::set &input_candidate_subset : input_candidate_subsets) { - // Return early if already selected inputs or no available selectable - const bool already_selected = !selected_inputs_indices.empty(); - if (already_selected || selectable_indices.empty()) - return; + if (selected_inputs_indices.size()) break; - // Return early if not enough money in this selectable set... + // Skip if not enough money in this selectable set for max number of tx inputs... const auto max_usable_money = input_count_for_max_usable_money(input_candidates, - selectable_indices, - fee_by_input_count); - const bool enough_money = max_usable_money.first > 0 - && max_usable_money.second >= required_money_by_input_count.at(max_usable_money.first); - if (!enough_money) - return; + input_candidate_subset, FCMP_PLUS_PLUS_MAX_INPUTS, fee_by_input_count); + if (!max_usable_money.first) + continue; + else if (max_usable_money.second < required_money_by_input_count.at(max_usable_money.first)) + continue; - const boost::multiprecision::uint128_t max_usable_money_u128 = - boost::numeric_cast(max_usable_money.second); - MDEBUG("Trying to dispatch input selection on " << selectable_indices.size() << - "-input subset with max usable money: " << cryptonote::print_money(max_usable_money_u128) << " XMR"); - for (const size_t selectable_index : selectable_indices) + // Debug log input candidate subset + MDEBUG("Trying to dispatch input selection on " << input_candidate_subset.size() << + "-input subset with tx max usable money: " << cryptonote::print_money(max_usable_money.second)); + for (const std::size_t selectable_index : input_candidate_subset) { const CarrotPreSelectedInput &input_candidate = input_candidates[selectable_index]; MDEBUG(" " << input_candidate); } - // For each passed policy and while not already selected inputs, dispatch policy... - for (size_t policy_idx = 0; policy_idx < policies.size() && selected_inputs_indices.empty(); ++policy_idx) - policies[policy_idx](input_candidates, - selectable_indices, - required_money_by_input_count, - selected_inputs_indices); + // For each transaction input count... + for (const std::size_t n_inputs : input_counts) + { + if (selected_inputs_indices.size()) break; - // Check that returned selected indices were actually selectable - for (const size_t selected_inputs_index : selected_inputs_indices) - CHECK_AND_ASSERT_THROW_MES(selectable_indices.count(selected_inputs_index), - "make_single_transfer_input_selector: bug in policy: returned unselectable index"); + const boost::multiprecision::uint128_t &required_money = required_money_by_input_count.at(n_inputs); + + // Skip if not enough money in this selectable set for exact number of inputs... + const auto max_usable_money = input_count_for_max_usable_money(input_candidates, + input_candidate_subset, n_inputs, fee_by_input_count); + if (max_usable_money.first != n_inputs) + continue; + else if (max_usable_money.second < required_money) + continue; + + // After this point, we expect one of the policies to succeed, otherwise all input selection fails + + // at least one call to an input selection subroutine has enough usable money to work with + MDEBUG("Trying input selection with " << n_inputs << " tx inputs"); + + // Filter all dust out of subset unless ALLOW_DUST flag is provided + std::set candidate_subset_filtered = input_candidate_subset; + if (!(flags * ALLOW_DUST)) + { + const rct::xmr_amount dust_threshold = fee_by_input_count.at(n_inputs) + - (n_inputs > CARROT_MIN_TX_INPUTS ? fee_by_input_count.at(n_inputs - 1) : 0); + for (auto it = candidate_subset_filtered.cbegin(); it != candidate_subset_filtered.cend();) + { + if (*it >= input_candidates.size() || input_candidates[*it].core.amount <= dust_threshold) + it = candidate_subset_filtered.erase(it); + else + ++it; + } + } + + // For each input selection policy... + for (const input_selection_policy_t &policy : policies) + { + if (selected_inputs_indices.size()) break; + + policy(input_candidates, + candidate_subset_filtered, + n_inputs, + required_money, + selected_inputs_indices); + } + + // Check nominal success + CARROT_CHECK_AND_THROW(selected_inputs_indices.size(), + carrot_runtime_error, "provided input selection policies failed with enough usable money"); + CARROT_CHECK_AND_THROW(selected_inputs_indices.size() == n_inputs, + carrot_logic_error, "bug in policy: selected wrong number of inputs"); + + // Check selected indices were actually selectable + for (const std::size_t selected_inputs_index : selected_inputs_indices) + CARROT_CHECK_AND_THROW(candidate_subset_filtered.count(selected_inputs_index), + carrot_logic_error, "bug in policy: returned unselectable index"); + } }; - // 8. Try dispatching for non-forward-secret input subsets, if preferred in this context - if (prefer_non_fs) - { - // try getting rid of pre-carrot enotes first, if allowed - if (!must_use_carrot) - try_dispatch_input_selection(pre_carrot_inputs); + // 8. Sanity check indices + CARROT_CHECK_AND_THROW(!selected_inputs_indices.empty(), + not_enough_usable_money, + "No single allowed subset of candidates had enough money to fund payment proposals and fees for inputs"); + CARROT_CHECK_AND_THROW(*selected_inputs_indices.crbegin() < input_candidates.size(), + carrot_logic_error, "bug: selected inputs index out of range"); - // ... then external carrot - try_dispatch_input_selection(external_carrot_inputs); - } - - // 9. Try dispatching for internal - try_dispatch_input_selection(internal_inputs); - - // 10. Try dispatching for non-FS *after* internal, if allowed and not already tried - if (!must_use_internal || !prefer_non_fs) - { - // Spending non-FS inputs in a normal transfer transaction is not ideal, but at least - // when partition it like this, we aren't "dirtying" the carrot with the pre-carrot, and - // the internal with the external - if (!must_use_carrot) - try_dispatch_input_selection(pre_carrot_inputs); - try_dispatch_input_selection(external_carrot_inputs); - } - - // 11. Try dispatching for all non-FS (mixed pre-carrot & carrot external), if allowed - if (allow_mixed_carrotness) - { - // We're mixing carrot/pre-carrot spends here, but avoiding "dirtying" the internal - try_dispatch_input_selection(set_union(pre_carrot_inputs, external_carrot_inputs)); - } - - // 12. Try dispatching for all carrot, if allowed - if (allow_mixed_externality) - { - // We're mixing internal & external carrot spends here, but avoiding "dirtying" the - // carrot spends with pre-carrot spends. This will be quantum forward secret iff the - // adversary doesn't know one of your public addresses - try_dispatch_input_selection(set_union(external_carrot_inputs, internal_inputs)); - } - - //! @TODO: MRL discussion about whether step 11 or step 12 should go first. In other words, - // do we prefer to avoid dirtying internal, and protect against quantum adversaries - // who know your public addresses? Or do we prefer to avoid dirtying w/ pre-carrot, - // and protect against quantum adversaries with no special knowledge of your public - // addresses, but whose attacks are only relevant when spending pre-FCMP++ enotes? - - // 13. Try dispatching for everything, if allowed - if (allow_mixed_carrotness && allow_mixed_externality) - try_dispatch_input_selection(all_non_burned_inputs); - - // Notice that we don't combine just the pre_carrot_inputs and internal_inputs by themselves - - // 14. Sanity check indices - CHECK_AND_ASSERT_THROW_MES(!selected_inputs_indices.empty(), - "make_single_transfer_input_selector: input selection failed"); - CHECK_AND_ASSERT_THROW_MES(*selected_inputs_indices.crbegin() < input_candidates.size(), - "make_single_transfer_input_selector: bug: selected inputs index out of range"); - - // 15. Do a greedy search for inputs whose amount doesn't pay for itself and drop them, logging debug messages - // Note: this also happens to be optimal if the fee difference between each input count is constant - bool should_search_for_dust = !(flags & ALLOW_DUST); - while (should_search_for_dust && selected_inputs_indices.size() > CARROT_MIN_TX_INPUTS) - { - should_search_for_dust = false; // only loop again if we remove an input below - const boost::multiprecision::int128_t fee_diff = - required_money_by_input_count.at(selected_inputs_indices.size()) - - required_money_by_input_count.at(selected_inputs_indices.size() - 1); - CHECK_AND_ASSERT_THROW_MES(fee_diff >= 0, - "make_single_transfer_input_selector: bug: fee is expected to be higher with fewer inputs"); - for (auto it = selected_inputs_indices.begin(); it != selected_inputs_indices.end(); ++it) - { - const CarrotPreSelectedInput &input_candidate = input_candidates[*it]; - if (input_candidate.core.amount < fee_diff) - { - MDEBUG("make_single_transfer_input_selector: dropping dusty input " - << input_candidate.core.key_image << " with amount " << input_candidate.core.amount - << ", which is less than the difference in fee of this transaction with it: " << fee_diff); - selected_inputs_indices.erase(it); - should_search_for_dust = true; - break; // break out of inner `for` loop so we can recalculate `fee_diff` - } - } - } - - // 16. Check the sum of input amounts is great enough - const size_t num_selected = selected_inputs_indices.size(); - const boost::multiprecision::int128_t required_money = required_money_by_input_count.at(num_selected); - boost::multiprecision::int128_t input_amount_sum = 0; - for (const size_t idx : selected_inputs_indices) + // 9. Check the sum of input amounts is great enough + const std::size_t num_selected = selected_inputs_indices.size(); + const boost::multiprecision::uint128_t required_money = required_money_by_input_count.at(num_selected); + boost::multiprecision::uint128_t input_amount_sum = 0; + for (const std::size_t idx : selected_inputs_indices) input_amount_sum += input_candidates[idx].core.amount; - CHECK_AND_ASSERT_THROW_MES(input_amount_sum >= required_money, - "make_single_transfer_input_selector: bug: input selection returned successful without enough funds"); + CARROT_CHECK_AND_THROW(input_amount_sum >= required_money, + carrot_logic_error, "bug: input selection returned successful without enough funds"); - // 17. Collect selected inputs + // 10. Collect selected inputs selected_inputs_out.clear(); selected_inputs_out.reserve(num_selected); for (size_t selected_input_index : selected_inputs_indices) @@ -422,100 +531,19 @@ select_inputs_func_t make_single_transfer_input_selector( namespace ispolicy { //------------------------------------------------------------------------------------------------------------------- -std::vector get_input_counts_in_preferred_order() -{ - // 1 or 2 randomly, then - // other ascending non-zero powers of 2, then - // other ascending non-zero numbers - - //! @TODO: MRL discussion about 2 vs 1 default input count when 1 input can pay. If we default to 1, then that may - // reveal more information about the amount, and reveals that one can't pay with 1 output when using 2. Vice versa, - // if we default to 2, then that means that one only owns 1 output when using 1. It may be the most advantageous to - // randomly switch between preferring 1 vs 2. See: https://lavalle.pl/planning/node437.html - - static_assert(CARROT_MAX_TX_INPUTS == FCMP_PLUS_PLUS_MAX_INPUTS, "inconsistent input count max limit"); - static_assert(CARROT_MIN_TX_INPUTS == 1 && CARROT_MAX_TX_INPUTS == 8, - "refactor this function for different input count limits"); - - const bool random_bit = 0 == (crypto::rand() & 0x01); - if (random_bit) - return {2, 1, 4, 8, 3, 5, 6, 7}; - else - return {1, 2, 4, 8, 3, 5, 6, 7}; -} -//------------------------------------------------------------------------------------------------------------------- -void select_two_inputs_prefer_oldest(const epee::span input_candidates, - const std::set &selectable_inputs, - const std::map &required_money_by_input_count, - std::set &selected_inputs_indices_out) -{ - // calculate required money and fee diff from one to two inputs - const boost::multiprecision::int128_t required_money = required_money_by_input_count.at(2); - const rct::xmr_amount fee_diff = boost::numeric_cast(required_money - - required_money_by_input_count.at(1)); - - // copy selectable_inputs, excluding dust, then sort by ascending block index - std::vector selectable_inputs_by_bi; - selectable_inputs_by_bi.reserve(selectable_inputs.size()); - for (size_t idx : selectable_inputs) - if (input_candidates[idx].core.amount > fee_diff) - selectable_inputs_by_bi.push_back(idx); - stable_sort_indices_by_block_index(input_candidates, selectable_inputs_by_bi); - - // then copy again and *stable* sort by amount - std::vector selectable_inputs_by_amount_bi = selectable_inputs_by_bi; - stable_sort_indices_by_amount(input_candidates, selectable_inputs_by_amount_bi); - - // for each input in ascending block index order... - for (size_t low_bi_input : selectable_inputs_by_bi) - { - // calculate how much we need in a corresponding input to this one - const rct::xmr_amount old_amount = input_candidates[low_bi_input].core.amount; - const boost::multiprecision::int128_t required_money_in_other_128 = (required_money > old_amount) - ? (required_money - old_amount) : 0; - if (required_money_in_other_128 >= std::numeric_limits::max()) - continue; - const rct::xmr_amount required_money_in_other = - boost::numeric_cast(required_money_in_other_128); - - // do a binary search for an input with at least that amount - auto other_it = std::lower_bound(selectable_inputs_by_amount_bi.cbegin(), - selectable_inputs_by_amount_bi.cend(), - required_money_in_other, - [input_candidates](size_t selectable_index, rct::xmr_amount required_money_in_other) -> bool - { return input_candidates[selectable_index].core.amount < required_money_in_other; }); - - // check that the iterator is in bounds and the complementary input isn't equal to the first - if (other_it == selectable_inputs_by_amount_bi.cend()) - continue; - else if (*other_it == low_bi_input) - ++other_it; // can't choose same input twice - - if (other_it == selectable_inputs_by_amount_bi.cend()) - continue; - - // we found a match ! - selected_inputs_indices_out = {low_bi_input, *other_it}; - return; - } -} -//------------------------------------------------------------------------------------------------------------------- -void select_greedy_aging_fixed_count(const std::size_t fixed_n_inputs, - const epee::span input_candidates, +void select_greedy_aging(const epee::span input_candidates, const std::set &selectable_inputs, - const std::map &required_money_by_input_count, + const std::size_t n_inputs, + const boost::multiprecision::uint128_t &required_money, std::set &selected_inputs_indices_out) { - MTRACE(__func__ << ": fixed_n_inputs=" << fixed_n_inputs << ", selectable_inputs.size()=" - << selectable_inputs.size()); + MTRACE(__func__ << ": n_inputs=" << n_inputs << ", selectable_inputs.size()=" << selectable_inputs.size()); selected_inputs_indices_out.clear(); - CHECK_AND_ASSERT_MES(fixed_n_inputs,, "select_greedy_aging: fixed_n_inputs must be non-zero"); - CHECK_AND_ASSERT_MES(fixed_n_inputs <= selectable_inputs.size(),, - "select_greedy_aging: not enough inputs: " << selectable_inputs.size() << '/' << fixed_n_inputs); - CHECK_AND_ASSERT_MES(required_money_by_input_count.count(fixed_n_inputs),, - "select_greedy_aging: input count " << fixed_n_inputs << "not allowed"); + CHECK_AND_ASSERT_MES(n_inputs,, "select_greedy_aging: n_inputs must be non-zero"); + CHECK_AND_ASSERT_MES(n_inputs <= selectable_inputs.size(),, + "select_greedy_aging: not enough inputs: " << selectable_inputs.size() << '/' << n_inputs); // Sort selectable inputs by amount std::vector selectable_inputs_by_amount(selectable_inputs.cbegin(), selectable_inputs.cend()); @@ -524,7 +552,7 @@ void select_greedy_aging_fixed_count(const std::size_t fixed_n_inputs, // Select highest amount inputs and collect ordered multi-map of block indices of current selected inputs boost::multiprecision::uint128_t input_amount_sum = 0; std::multimap selected_indices_by_block_index; - for (size_t i = 0; i < fixed_n_inputs; ++i) + for (size_t i = 0; i < n_inputs; ++i) { const std::size_t selectable_idx = selectable_inputs_by_amount.at(selectable_inputs_by_amount.size() - i - 1); const CarrotPreSelectedInput &input = input_candidates[selectable_idx]; @@ -534,11 +562,9 @@ void select_greedy_aging_fixed_count(const std::size_t fixed_n_inputs, } // Check enough money - const boost::multiprecision::uint128_t required_money = - boost::numeric_cast(required_money_by_input_count.at(fixed_n_inputs)); if (input_amount_sum < required_money) { - MDEBUG("not enough money in " << fixed_n_inputs << " inputs: " << cryptonote::print_money(input_amount_sum)); + MDEBUG("not enough money in " << n_inputs << " inputs: " << cryptonote::print_money(input_amount_sum)); selected_inputs_indices_out.clear(); return; } @@ -548,7 +574,7 @@ void select_greedy_aging_fixed_count(const std::size_t fixed_n_inputs, for (auto bi_it = selected_indices_by_block_index.rbegin(); bi_it != selected_indices_by_block_index.rend();) { std::uint64_t min_block_index = bi_it->first; - size_t input_of_min_block_index_input = bi_it->second; + std::size_t input_of_min_block_index_input = bi_it->second; const boost::multiprecision::uint128_t surplus = input_amount_sum - required_money; const rct::xmr_amount currently_selected_amount = input_candidates[bi_it->second].core.amount; const rct::xmr_amount lowest_replacement_amount = (currently_selected_amount > surplus) @@ -576,8 +602,8 @@ void select_greedy_aging_fixed_count(const std::size_t fixed_n_inputs, selected_indices_by_block_index.emplace(min_block_index, input_of_min_block_index_input); input_amount_sum -= currently_selected_amount; input_amount_sum += input_candidates[input_of_min_block_index_input].core.amount; - CHECK_AND_ASSERT_THROW_MES(input_amount_sum >= required_money, - "select_greedy_aging: BUG: replaced an input with one of too low amount"); + CARROT_CHECK_AND_THROW(input_amount_sum >= required_money, + carrot_logic_error, "BUG: replaced an input with one of too low amount"); } else // no replacement, go to next input { @@ -586,24 +612,5 @@ void select_greedy_aging_fixed_count(const std::size_t fixed_n_inputs, } } //------------------------------------------------------------------------------------------------------------------- -void select_greedy_aging(const epee::span input_candidates, - const std::set &selectable_inputs, - const std::map &required_money_by_input_count, - std::set &selected_inputs_indices_out) -{ - selected_inputs_indices_out.clear(); - - for (const std::size_t n_inputs : get_input_counts_in_preferred_order()) - { - select_greedy_aging_fixed_count(n_inputs, - input_candidates, - selectable_inputs, - required_money_by_input_count, - selected_inputs_indices_out); - if (!selected_inputs_indices_out.empty()) - return; - } -} -//------------------------------------------------------------------------------------------------------------------- } //namespace ispolicy } //namespace carrot diff --git a/src/carrot_impl/input_selection.h b/src/carrot_impl/input_selection.h index a9fba122d..b21ec9309 100644 --- a/src/carrot_impl/input_selection.h +++ b/src/carrot_impl/input_selection.h @@ -63,12 +63,89 @@ namespace InputSelectionFlags } using input_selection_policy_t = std::function, // input candidates - const std::set&, // selectable subset indices - const std::map&, // required money by input count - std::set& // selected indices + epee::span, // input candidates + const std::set&, // selectable subset indices + std::size_t, // input count + const boost::multiprecision::uint128_t&, // required money for input count + std::set& // selected indices )>; +/** + * brief: compare_input_candidate_same_ki - compare two input candidates who share a key image; we can only choose one! + * param: lhs - + * param: rhs - + * return: 1 if lhs is better, -1 if rhs is better, 0 if neutral + * throw: component_out_of_order - iff rhs.core.key_image != lhs.core.key_image + * + * The better candidate is determined by criteria in descending order of importance as follows: + * 1. Amount (higher is better, duh) + * 2. Age (older is better for protection against double spend attacks) + * 3. Is pre-Carrot enote? (`false` is better for spending QFS) + * 4. Is external enote? (`false` is better for spending QFS) + */ +int compare_input_candidate_same_ki(const CarrotPreSelectedInput &lhs, const CarrotPreSelectedInput &rhs); +/** + * brief: form_preferred_input_candidate_subsets - make subsets of input candidates to try selection in preferred order + * param: input_candidates - slice to user-provided input candidates + * param: flags - values in InputSelectionFlags namespace OR'ed together + * param: is_normal_transfer - true iff num normal non-dummy payments in tx to perform selection for is >= 1 + * return: ordered list of subsets (represented by 0-based indices) of input_candidates to try selection on + * + * This function also performs a burning bug check; no indices returned in any subset will reference + * an input candidate when another input candidate shares the same key image but is "better" as + * determined by compare_input_candidate_same_ki. + * + * Purpose: Mainly due to quantum forward secrecy properties of spending different types of Monero + * enotes, it isn't always preferable to be able to select all usable input candidates together in + * the same transaction. For example, spending internal Carrot enotes is always quantum forward + * secret, even when one's public Monero address is known. By contrast, spending a pre-Carrot enote + * is never quantum forward secret, even with no public address knowledge. As such, a spender should + * prefer not to spend these enotes in the same transaction, since the pre-Carrot enote will "taint" + * the quantum forward spending secrecy of the internal Carrot enote. Based on certain heuristics + * and user-provided flags, this function creates a list of subsets of input_candidates, in + * preferred order, to perform input selection on. + * + * Flags: + * * ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS - external inputs in normal txs are allowed iff=1 + * * ALLOW_PRE_CARROT_INPUTS_IN_NORMAL_TRANSFERS - pre-carrot inputs in normal txs are allowed iff=1 + * * ALLOW_MIXED_INTERNAL_EXTERNAL - mixing internal/external inputs in any txs is allowed iff=1 + * * ALLOW_MIXED_CARROT_PRE_CARROT - mixing pre-carrot/carrot inputs in any txs is allowed iff=1 + * + * General rules (not in any specific order): + * * It should be preferred NOT to use external inputs in normal transfers + * * It should be preferred NOT to use pre-carrot inputs in normal transfers + * * It should be preferred NOT to mix internal/external inputs + * * It should be preferred NOT to mix pre-carrot/carrot inputs + * * It should be preferred YES to use external & pre-carrots inputs in self-send transactions + * + * In scenarios where a user-provided flag allows a non-preferred subset of input candidates, this + * function will FIRST add the subset of input candidates as if the user didn't provide that + * flag, and THEN add the non-preferred subset. For example, let's say that you pass input + * candidates span {A, B}, flags=ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS, and + * is_normal_transfer=true. In this example, A is internal and B is external. The return + * value of this function will be {{0}, {1}, {0, 1}}. This means: "try input selection on 0 (A) + * first, then 1 (B), and then both {0, 1} (A & B)". In this example, although external inputs are + * *allowed* in normal transfers by the flag provided by the user, they are not *preferred*, so + * {A} comes before {B} in the subset list. Mixing is the least preferred, so {A, B} is the last + * subset. + * + * If unsure which flags to use, flags=0 is the "safest" option for input selection. Note that this + * completely disallows normal transfers for legacy key hierarchies, since inputs will never be + * internal due to the lack of the view-balance secret s_vb in legacy key hierarchies. + */ +std::vector> form_preferred_input_candidate_subsets( + const epee::span input_candidates, + const std::uint32_t flags, + const bool is_normal_transfer); +/** + * brief: get_input_counts_in_preferred_order - return list of tx input counts in order that we should prefer to select + * + * Transaction input counts are trivially observable on-chain, so picking a wrong input count when + * given the chance between multiple choices can have privacy consequences. The purpose of this + * function is to determine the order in which we should try to select a certain number of inputs. + */ +std::vector get_input_counts_in_preferred_order(); + select_inputs_func_t make_single_transfer_input_selector( const epee::span input_candidates, const epee::span policies, @@ -77,23 +154,10 @@ select_inputs_func_t make_single_transfer_input_selector( namespace ispolicy { -std::vector get_input_counts_in_preferred_order(); - -void select_two_inputs_prefer_oldest( - const epee::span, - const std::set&, - const std::map&, - std::set&); - -void select_greedy_aging_fixed_count(const std::size_t fixed_n_inputs, - const epee::span, - const std::set&, - const std::map&, - std::set&); - void select_greedy_aging(const epee::span, const std::set&, - const std::map&, + std::size_t, + const boost::multiprecision::uint128_t&, std::set&); } //namespace ispolicy diff --git a/src/wallet/tx_builder.cpp b/src/wallet/tx_builder.cpp index 98fa65267..206afcdd0 100644 --- a/src/wallet/tx_builder.cpp +++ b/src/wallet/tx_builder.cpp @@ -34,6 +34,7 @@ #include "carrot_core/device_ram_borrowed.h" #include "carrot_core/enote_utils.h" #include "carrot_impl/carrot_tx_builder_utils.h" +#include "carrot_impl/carrot_tx_format_utils.h" #include "carrot_impl/input_selection.h" #include "cryptonote_basic/cryptonote_format_utils.h" #include "common/container_helpers.h" @@ -230,7 +231,7 @@ carrot::select_inputs_func_t make_wallet2_single_transfer_input_selector( .amount = td.amount(), .key_image = td.m_key_image }, - .is_pre_carrot = true, //! @TODO: handle post-Carrot enotes in transfer_details + .is_pre_carrot = !carrot::is_carrot_transaction_v1(td.m_tx), .is_external = true, //! @TODO: derive this info from field in transfer_details .block_index = td.m_block_height }); @@ -245,7 +246,7 @@ carrot::select_inputs_func_t make_wallet2_single_transfer_input_selector( allow_pre_carrot_inputs_in_normal_transfers, &selected_transfer_indices_out ]( - const boost::multiprecision::int128_t& nominal_output_sum, + const boost::multiprecision::uint128_t &nominal_output_sum, const std::map &fee_by_input_count, const std::size_t num_normal_payment_proposals, const std::size_t num_selfsend_payment_proposals, @@ -255,7 +256,6 @@ carrot::select_inputs_func_t make_wallet2_single_transfer_input_selector( &carrot::ispolicy::select_greedy_aging }; - // TODO: not all carrot is internal std::uint32_t flags = 0; if (allow_carrot_external_inputs_in_normal_transfers) flags |= carrot::InputSelectionFlags::ALLOW_EXTERNAL_INPUTS_IN_NORMAL_TRANSFERS; diff --git a/tests/unit_tests/carrot_impl.cpp b/tests/unit_tests/carrot_impl.cpp index 9f840c52d..621b1bff6 100644 --- a/tests/unit_tests/carrot_impl.cpp +++ b/tests/unit_tests/carrot_impl.cpp @@ -30,6 +30,7 @@ #include +#include "carrot_core/exceptions.h" #include "carrot_core/output_set_finalization.h" #include "carrot_core/payment_proposal.h" #include "carrot_impl/carrot_tx_builder_utils.h" @@ -109,7 +110,7 @@ struct unittest_transaction_preproposal //---------------------------------------------------------------------------------------------------------------------- select_inputs_func_t make_fake_input_selection_callback(size_t num_ins = 0) { - return [num_ins](const boost::multiprecision::int128_t &nominal_output_sum, + return [num_ins](const boost::multiprecision::uint128_t &nominal_output_sum, const std::map &fee_per_input_count, size_t, size_t, @@ -1164,28 +1165,14 @@ TEST(carrot_impl, multi_account_transfer_over_transaction_16) subtest_multi_account_transfer_over_transaction(tx_proposal); } //---------------------------------------------------------------------------------------------------------------------- -TEST(carrot_impl, make_single_transfer_input_selector_TwoInputsPreferOldest_1) +TEST(carrot_impl, make_single_transfer_input_selector_not_enough_money_1) { + // no input candidates, should throw `not_enough_money` + const std::vector input_candidates = { - CarrotPreSelectedInput { - .core = CarrotSelectedInput { - .amount = 500, - .key_image = mock::gen_key_image(), - }, - .is_external = false, - .block_index = 72 - }, - CarrotPreSelectedInput { - .core = CarrotSelectedInput { - .amount = 200, - .key_image = mock::gen_key_image(), - }, - .is_external = false, - .block_index = 34 - } }; - const std::vector policies = { &carrot::ispolicy::select_two_inputs_prefer_oldest }; + const std::vector policies = { input_selection_policy_t{} }; const uint32_t flags = 0; @@ -1195,7 +1182,7 @@ TEST(carrot_impl, make_single_transfer_input_selector_TwoInputsPreferOldest_1) flags, &selected_input_indices); - boost::multiprecision::int128_t nominal_output_sum = 369; + const boost::multiprecision::uint128_t nominal_output_sum = 369; const std::map fee_by_input_count = { {1, 50}, @@ -1205,21 +1192,119 @@ TEST(carrot_impl, make_single_transfer_input_selector_TwoInputsPreferOldest_1) const size_t num_normal_payment_proposals = 1; const size_t num_selfsend_payment_proposals = 1; - ASSERT_GT(input_candidates[0].core.amount, nominal_output_sum + fee_by_input_count.crbegin()->second); + std::vector selected_inputs; + EXPECT_THROW(input_selector(nominal_output_sum, + fee_by_input_count, + num_normal_payment_proposals, + num_selfsend_payment_proposals, + selected_inputs), + not_enough_money); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, make_single_transfer_input_selector_not_enough_money_2) +{ + // 1 input candidates w/ strictly less than nominal output sum, should throw `not_enough_money` + + const std::vector input_candidates = { + CarrotPreSelectedInput { + .core = CarrotSelectedInput { + .amount = 222, + .key_image = mock::gen_key_image(), + }, + .is_external = false, + .block_index = 23 + }, + }; + + const std::vector policies = { input_selection_policy_t{} }; + + const uint32_t flags = 0; + + std::set selected_input_indices; + select_inputs_func_t input_selector = make_single_transfer_input_selector(epee::to_span(input_candidates), + epee::to_span(policies), + flags, + &selected_input_indices); + + const boost::multiprecision::uint128_t nominal_output_sum = 369; + + const std::map fee_by_input_count = { + {1, 50}, + {2, 75} + }; + + const size_t num_normal_payment_proposals = 1; + const size_t num_selfsend_payment_proposals = 1; std::vector selected_inputs; - input_selector(nominal_output_sum, - fee_by_input_count, - num_normal_payment_proposals, - num_selfsend_payment_proposals, - selected_inputs); + EXPECT_THROW(input_selector(nominal_output_sum, + fee_by_input_count, + num_normal_payment_proposals, + num_selfsend_payment_proposals, + selected_inputs), + not_enough_money); +} +//---------------------------------------------------------------------------------------------------------------------- +TEST(carrot_impl, make_single_transfer_input_selector_not_enough_money_3) +{ + // 2 input candidates who sum is strictly greater than the required money for a 1-in tx, but + // strictly less than the required money for a 2-out tx, should throw `not_enough_usable_money` - ASSERT_EQ(2, input_candidates.size()); - ASSERT_EQ(2, selected_inputs.size()); - EXPECT_NE(input_candidates.at(0).core, input_candidates.at(1).core); - EXPECT_NE(selected_inputs.at(0), selected_inputs.at(1)); - EXPECT_TRUE((selected_inputs.at(0) == input_candidates.at(0).core) ^ (selected_inputs.at(0) == input_candidates[1].core)); - EXPECT_TRUE((selected_inputs.at(1) == input_candidates.at(0).core) ^ (selected_inputs.at(1) == input_candidates.at(1).core)); + const rct::xmr_amount nominal_output_sum = 369; + + const std::map fee_by_input_count = { + {1, 50}, + {2, 75} + }; + + const rct::xmr_amount required_1in = fee_by_input_count.at(1) + nominal_output_sum; + const rct::xmr_amount required_2in = fee_by_input_count.at(2) + nominal_output_sum; + ASSERT_GT(required_1in, 0); + ASSERT_GT(required_2in, required_1in + 1); + + const rct::xmr_amount input_sum_target = (required_2in + required_1in) / 2; + const rct::xmr_amount inamount_0 = rct::randXmrAmount(required_1in); + const rct::xmr_amount inamount_1 = input_sum_target - inamount_0; + + const std::vector input_candidates = { + CarrotPreSelectedInput { + .core = CarrotSelectedInput { + .amount = inamount_0, + .key_image = mock::gen_key_image(), + }, + .is_external = false, + .block_index = 3407684 + }, + CarrotPreSelectedInput { + .core = CarrotSelectedInput { + .amount = inamount_1, + .key_image = mock::gen_key_image(), + }, + .is_external = false, + .block_index = 4867043 + }, + }; + + const std::vector policies = { input_selection_policy_t{} }; + + const uint32_t flags = 0; + + std::set selected_input_indices; + select_inputs_func_t input_selector = make_single_transfer_input_selector(epee::to_span(input_candidates), + epee::to_span(policies), + flags, + &selected_input_indices); + + const size_t num_normal_payment_proposals = 1; + const size_t num_selfsend_payment_proposals = 1; + + std::vector selected_inputs; + EXPECT_THROW(input_selector(nominal_output_sum, + fee_by_input_count, + num_normal_payment_proposals, + num_selfsend_payment_proposals, + selected_inputs), + not_enough_usable_money); } //---------------------------------------------------------------------------------------------------------------------- TEST(carrot_impl, make_single_transfer_input_selector_greedy_aging_1) @@ -1253,7 +1338,7 @@ TEST(carrot_impl, make_single_transfer_input_selector_greedy_aging_1) flags, &selected_input_indices); - boost::multiprecision::int128_t nominal_output_sum = 369; + const boost::multiprecision::uint128_t nominal_output_sum = 369; const std::map fee_by_input_count = { {1, 50}, @@ -1312,7 +1397,7 @@ TEST(carrot_impl, make_single_transfer_input_selector_greedy_aging_2) flags, &selected_input_indices); - boost::multiprecision::int128_t nominal_output_sum = 369; + const boost::multiprecision::uint128_t nominal_output_sum = 369; const std::map fee_by_input_count = { {1, 50}, @@ -1347,6 +1432,7 @@ TEST(carrot_impl, make_single_transfer_input_selector_greedy_aging_3) .amount = 100, .key_image = mock::gen_key_image(), }, + .is_pre_carrot = false, .is_external = false, .block_index = 55 }, @@ -1355,6 +1441,7 @@ TEST(carrot_impl, make_single_transfer_input_selector_greedy_aging_3) .amount = 100, .key_image = mock::gen_key_image(), }, + .is_pre_carrot = false, .is_external = false, .block_index = 22 }, @@ -1363,6 +1450,7 @@ TEST(carrot_impl, make_single_transfer_input_selector_greedy_aging_3) .amount = 100, .key_image = mock::gen_key_image(), }, + .is_pre_carrot = false, .is_external = false, .block_index = 11 }, @@ -1371,6 +1459,7 @@ TEST(carrot_impl, make_single_transfer_input_selector_greedy_aging_3) .amount = 100, .key_image = mock::gen_key_image(), }, + .is_pre_carrot = false, .is_external = false, .block_index = 88 }, @@ -1379,6 +1468,7 @@ TEST(carrot_impl, make_single_transfer_input_selector_greedy_aging_3) .amount = 100, .key_image = mock::gen_key_image(), }, + .is_pre_carrot = false, .is_external = false, .block_index = 72 } @@ -1394,7 +1484,7 @@ TEST(carrot_impl, make_single_transfer_input_selector_greedy_aging_3) flags, &selected_input_indices); - boost::multiprecision::int128_t nominal_output_sum = 223; + const boost::multiprecision::uint128_t nominal_output_sum = 223; const std::map fee_by_input_count = { {1, 10}, diff --git a/tests/unit_tests/wallet_tx_builder.cpp b/tests/unit_tests/wallet_tx_builder.cpp index 1d46314d8..6281c2cdc 100644 --- a/tests/unit_tests/wallet_tx_builder.cpp +++ b/tests/unit_tests/wallet_tx_builder.cpp @@ -38,9 +38,12 @@ static tools::wallet2::transfer_details gen_transfer_details() { + cryptonote::transaction carrot_tx; + carrot_tx.vout.push_back(cryptonote::tx_out{.target = cryptonote::txout_to_carrot_v1{}}); + return tools::wallet2::transfer_details{ .m_block_height = crypto::rand_idx(CRYPTONOTE_MAX_BLOCK_NUMBER), - .m_tx = {}, + .m_tx = carrot_tx, .m_txid = crypto::rand(), .m_internal_output_index = crypto::rand_idx(carrot::CARROT_MAX_TX_OUTPUTS), .m_global_output_index = crypto::rand_idx(CRYPTONOTE_MAX_BLOCK_NUMBER * 1000ull), @@ -74,7 +77,7 @@ TEST(wallet_tx_builder, input_selection_basic) for (size_t i = carrot::CARROT_MIN_TX_INPUTS; i <= carrot::CARROT_MAX_TX_INPUTS; ++i) fee_by_input_count[i] = 30680000 * i - i*i; - const boost::multiprecision::int128_t nominal_output_sum = 4444444444444; // 4.444... XMR + const boost::multiprecision::uint128_t nominal_output_sum = 4444444444444; // 4.444... XMR // add 10 random transfers tools::wallet2::transfer_container transfers;