diff --git a/tests/core_tests/chaingen.cpp b/tests/core_tests/chaingen.cpp index d6931f65..a2560f31 100644 --- a/tests/core_tests/chaingen.cpp +++ b/tests/core_tests/chaingen.cpp @@ -1421,7 +1421,7 @@ bool fill_tx_sources(std::vector& sources, const std::vector& sources, const std::vector& events, const currency::block& blk_head, const currency::account_keys& from, uint64_t amount, size_t nmix, const std::vector& sources_to_avoid, - bool check_for_spends, bool check_for_unlocktime, bool use_ref_by_id, uint64_t* p_sources_amount_found /* = nullptr */) + bool check_for_spends, bool check_for_unlocktime, bool use_ref_by_id, uint64_t* p_sources_amount_found /* = nullptr */, const mixins_per_input* nmix_map /* = nullptr */) { uint64_t fts_flags = (check_for_spends ? fts_check_for_spends : fts_none) | @@ -1429,17 +1429,18 @@ bool fill_tx_sources(std::vector& sources, const std: (use_ref_by_id ? fts_use_ref_by_id : fts_none) | fts_check_for_hf4_min_coinage; - return fill_tx_sources(sources, events, blk_head, from, amount, nmix, sources_to_avoid, fts_flags, p_sources_amount_found); + return fill_tx_sources(sources, events, blk_head, from, amount, nmix, sources_to_avoid, fts_flags, p_sources_amount_found, nmix_map); } bool fill_tx_sources(std::vector& sources, const std::vector& events, const currency::block& blk_head, const currency::account_keys& from, uint64_t amount, size_t nmix, - const std::vector& sources_to_avoid, uint64_t fts_flags, uint64_t* p_sources_amount_found /* = nullptr */) + const std::vector& sources_to_avoid, uint64_t fts_flags, uint64_t* p_sources_amount_found /* = nullptr */, + const mixins_per_input* nmix_map /* = nullptr */) { std::unordered_map amounts; amounts[native_coin_asset_id] = amount; std::unordered_map sources_amounts; - if (!fill_tx_sources(sources, events, blk_head, from, amounts, nmix, sources_to_avoid, fts_flags, &sources_amounts)) + if (!fill_tx_sources(sources, events, blk_head, from, amounts, nmix, sources_to_avoid, fts_flags, &sources_amounts, nmix_map)) return false; if (p_sources_amount_found) *p_sources_amount_found = sources_amounts[native_coin_asset_id]; @@ -1448,7 +1449,8 @@ bool fill_tx_sources(std::vector& sources, const std: bool fill_tx_sources(std::vector& sources, const std::vector& events, const currency::block& blk_head, const currency::account_keys& from, const std::unordered_map& amounts, size_t nmix, - const std::vector& sources_to_avoid, uint64_t fts_flags, std::unordered_map* p_sources_amounts /* = nullptr */) + const std::vector& sources_to_avoid, uint64_t fts_flags, std::unordered_map* p_sources_amounts /* = nullptr */, + const mixins_per_input* nmix_map = nullptr) { map_output_idx_t outs; map_output_t outs_mine; @@ -1558,7 +1560,23 @@ bool fill_tx_sources(std::vector& sources, const std: ts.real_out_amount_blinding_mask = oi.amount_blinding_mask; ts.real_output_in_tx_index = oi.out_no; ts.real_out_tx_key = get_tx_pub_key_from_extra(*oi.p_tx); // source tx public key - if (!fill_output_entries(outs[o.first], sender_out, nmix, fts_flags & fts_check_for_unlocktime, fts_flags & fts_use_ref_by_id, + + // If we use nmix_map, we should use local_nmix instead of nmix + // to allow different nmix for different inputs in the same transaction. + // If nmix_map is not provided, we use nmix as local_nmix. + size_t local_nmix = nmix; + if (nmix_map) + { + // Try to find an override for this input index 'i' + auto it = nmix_map->find(i); + if (it != nmix_map->end()) + { + local_nmix = it->second; + } + // leave local_nmix at its default + } + + if (!fill_output_entries(outs[o.first], sender_out, local_nmix, fts_flags & fts_check_for_unlocktime, fts_flags & fts_use_ref_by_id, next_block_height, head_block_ts, ts.real_output, ts.outputs)) { continue; @@ -1592,7 +1610,9 @@ bool fill_tx_sources_and_destinations(const std::vector& event bool check_for_spends, bool check_for_unlocktime, size_t minimum_sigs, - bool use_ref_by_id) + bool use_ref_by_id, + const mixins_per_input* nmix_map +) { CHECK_AND_ASSERT_MES(!to.empty(), false, "destination addresses vector is empty"); CHECK_AND_ASSERT_MES(amount + fee > amount, false, "amount + fee overflow!"); @@ -1601,7 +1621,7 @@ bool fill_tx_sources_and_destinations(const std::vector& event bool b_multisig = to.size() > 1; uint64_t source_amount_found = 0; - bool r = fill_tx_sources(sources, events, blk_head, from, amount + fee, nmix, std::vector(), check_for_spends, check_for_unlocktime, use_ref_by_id, &source_amount_found); + bool r = fill_tx_sources(sources, events, blk_head, from, amount + fee, nmix, std::vector(), check_for_spends, check_for_unlocktime, use_ref_by_id, &source_amount_found, nmix_map); CHECK_AND_ASSERT_MES(r, false, "couldn't fill transaction sources (nmix = " << nmix << "): " << ENDL << " required: " << print_money(amount + fee) << " = " << std::fixed << std::setprecision(1) << ceil(1.0 * (amount + fee) / TESTS_DEFAULT_FEE) << " x TESTS_DEFAULT_FEE" << ENDL << " found coins: " << print_money(source_amount_found) << " = " << std::fixed << std::setprecision(1) << ceil(1.0 * source_amount_found / TESTS_DEFAULT_FEE) << " x TESTS_DEFAULT_FEE" << ENDL << @@ -1673,9 +1693,10 @@ bool fill_tx_sources_and_destinations(const std::vector& event std::vector& destinations, bool check_for_spends, bool check_for_unlocktime, - bool use_ref_by_id) + bool use_ref_by_id, + const mixins_per_input* nmix_map) { - return fill_tx_sources_and_destinations(events, blk_head, from, std::list({ to }), amount, fee, nmix, sources, destinations, check_for_spends, check_for_unlocktime, 0, use_ref_by_id); + return fill_tx_sources_and_destinations(events, blk_head, from, std::list({ to }), amount, fee, nmix, sources, destinations, check_for_spends, check_for_unlocktime, 0, use_ref_by_id, nmix_map); } bool fill_tx_sources_and_destinations(const std::vector& events, const currency::block& blk_head, @@ -1685,9 +1706,10 @@ bool fill_tx_sources_and_destinations(const std::vector& event std::vector& destinations, bool check_for_spends, bool check_for_unlocktime, - bool use_ref_by_id) + bool use_ref_by_id, + const mixins_per_input* nmix_map) { - return fill_tx_sources_and_destinations(events, blk_head, from.get_keys(), to.get_public_address(), amount, fee, nmix, sources, destinations, check_for_spends, check_for_unlocktime, use_ref_by_id); + return fill_tx_sources_and_destinations(events, blk_head, from.get_keys(), to.get_public_address(), amount, fee, nmix, sources, destinations, check_for_spends, check_for_unlocktime, use_ref_by_id, nmix_map); } /* diff --git a/tests/core_tests/chaingen.h b/tests/core_tests/chaingen.h index e9bf05a3..6bbda6e7 100644 --- a/tests/core_tests/chaingen.h +++ b/tests/core_tests/chaingen.h @@ -227,6 +227,7 @@ VARIANT_TAG(binary_archive, core_hardforks_config, 0xd2); typedef boost::variant test_event_entry; typedef std::unordered_map map_hash2tx_t; +typedef std::unordered_map mixins_per_input; enum test_tx_split_strategy { tests_default_split_strategy /*height-based, TODO*/, tests_void_split_strategy, tests_null_split_strategy, tests_digits_split_strategy, tests_random_split_strategy }; struct test_gentime_settings @@ -755,13 +756,16 @@ bool fill_tx_sources(std::vector& sources, const std: bool fill_tx_sources(std::vector& sources, const std::vector& events, const currency::block& blk_head, const currency::account_keys& from, uint64_t amount, size_t nmix, const std::vector& sources_to_avoid, bool check_for_spends = true, bool check_for_unlocktime = true, - bool use_ref_by_id = false, uint64_t* p_sources_amount_found = nullptr); + bool use_ref_by_id = false, uint64_t* p_sources_amount_found = nullptr, + const mixins_per_input* nmix_map = nullptr); bool fill_tx_sources(std::vector& sources, const std::vector& events, const currency::block& blk_head, const currency::account_keys& from, uint64_t amount, size_t nmix, - const std::vector& sources_to_avoid, uint64_t fts_flags, uint64_t* p_sources_amount_found = nullptr); + const std::vector& sources_to_avoid, uint64_t fts_flags, uint64_t* p_sources_amount_found = nullptr, + const mixins_per_input* nmix_map = nullptr); bool fill_tx_sources(std::vector& sources, const std::vector& events, const currency::block& blk_head, const currency::account_keys& from, const std::unordered_map& amounts, size_t nmix, - const std::vector& sources_to_avoid, uint64_t fts_flags, std::unordered_map* p_sources_amounts = nullptr); + const std::vector& sources_to_avoid, uint64_t fts_flags, std::unordered_map* p_sources_amounts = nullptr, + const mixins_per_input* nmix_map = nullptr); bool fill_tx_sources_and_destinations(const std::vector& events, const currency::block& blk_head, const currency::account_keys& from, const std::list& to, @@ -770,7 +774,8 @@ bool fill_tx_sources_and_destinations(const std::vector& event bool check_for_spends = true, bool check_for_unlocktime = true, size_t minimum_sigs = SIZE_MAX, - bool use_ref_by_id = false); + bool use_ref_by_id = false, + const mixins_per_input* nmix_map = nullptr); bool fill_tx_sources_and_destinations(const std::vector& events, const currency::block& blk_head, const currency::account_keys& from, const currency::account_public_address& to, uint64_t amount, uint64_t fee, size_t nmix, @@ -778,7 +783,8 @@ bool fill_tx_sources_and_destinations(const std::vector& event std::vector& destinations, bool check_for_spends = true, bool check_for_unlocktime = true, - bool use_ref_by_id = false); + bool use_ref_by_id = false, + const mixins_per_input* nmix_map = nullptr); bool fill_tx_sources_and_destinations(const std::vector& events, const currency::block& blk_head, const currency::account_base& from, const currency::account_base& to, uint64_t amount, uint64_t fee, size_t nmix, @@ -786,7 +792,8 @@ bool fill_tx_sources_and_destinations(const std::vector& event std::vector& destinations, bool check_for_spends = true, bool check_for_unlocktime = true, - bool use_ref_by_id = false); + bool use_ref_by_id = false, + const mixins_per_input* nmix_map = nullptr); uint64_t get_balance(const currency::account_keys& addr, const std::vector& blockchain, const map_hash2tx_t& mtx, bool dbg_log = false); uint64_t get_balance(const currency::account_base& addr, const std::vector& blockchain, const map_hash2tx_t& mtx, bool dbg_log = false); void balance_via_wallet(const tools::wallet2& w, const crypto::public_key& asset_id, uint64_t* p_total, uint64_t* p_unlocked = 0, uint64_t* p_awaiting_in = 0, uint64_t* p_awaiting_out = 0, uint64_t* p_mined = 0); diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index 978da46a..801aa2f8 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -1229,6 +1229,7 @@ int main(int argc, char* argv[]) GENERATE_AND_PLAY_HF(tx_pool_semantic_validation, "3"); GENERATE_AND_PLAY(input_refers_to_incompatible_by_type_output); GENERATE_AND_PLAY_HF(tx_pool_validation_and_chain_switch, "4-*"); + GENERATE_AND_PLAY(tx_input_mixins); // Double spend GENERATE_AND_PLAY(gen_double_spend_in_tx); diff --git a/tests/core_tests/tx_validation.cpp b/tests/core_tests/tx_validation.cpp index 91274083..b3458bac 100644 --- a/tests/core_tests/tx_validation.cpp +++ b/tests/core_tests/tx_validation.cpp @@ -1836,6 +1836,7 @@ bool tx_pool_semantic_validation::generate(std::vector& events } CHECK_AND_ASSERT_EQ(validate_tx_semantic(tx, CURRENCY_MAX_TRANSACTION_BLOB_SIZE - 1), true); + } // Two entries of the same type in extra. @@ -2728,3 +2729,101 @@ bool tx_pool_validation_and_chain_switch::c1(currency::core& c, size_t ev_index, return true; } +tx_input_mixins::tx_input_mixins() +{ + REGISTER_CALLBACK_METHOD(tx_input_mixins, configure_core); +} + +bool tx_input_mixins::configure_core(currency::core& c, size_t ev_index, const std::vector& events) +{ + currency::core_runtime_config pc = c.get_blockchain_storage().get_core_runtime_config(); + pc.min_coinstake_age = TESTS_POS_CONFIG_MIN_COINSTAKE_AGE; + pc.pos_minimum_heigh = TESTS_POS_CONFIG_POS_MINIMUM_HEIGH; + pc.hf4_minimum_mixins = 5; + pc.hard_forks.set_hardfork_height(1, 0); + pc.hard_forks.set_hardfork_height(2, 1); + pc.hard_forks.set_hardfork_height(3, 1); + pc.hard_forks.set_hardfork_height(4, 31); + c.get_blockchain_storage().set_core_runtime_config(pc); + return true; +} + +// Tests that post-HF4, legacy txin_to_key inputs accept any mixin count, while new txin_zc inputs enforce a >= 15 mixin minimum +// are in the same vin and work together +bool tx_input_mixins::generate(std::vector& events) const +{ + uint64_t ts = test_core_time::get_time(); + + GENERATE_ACCOUNT(miner_acc); + GENERATE_ACCOUNT(alice_acc); + GENERATE_ACCOUNT(bob_acc); + + m_hardforks.set_hardfork_height(1, 0); + m_hardforks.set_hardfork_height(2, 1); + m_hardforks.set_hardfork_height(3, 1); + m_hardforks.set_hardfork_height(4, 31); + MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, ts); + DO_CALLBACK(events, "configure_core"); + + // mine one block, then rewind enough to unlock initial coinbase funds + MAKE_NEXT_BLOCK(events, blk_1, blk_0, miner_acc); + REWIND_BLOCKS_N(events, blk_1r, blk_1, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW*2); + + // build tx_a_1: single txin_to_key output of 15 coins to Alice + transaction tx_a_1; + bool r = construct_tx_with_many_outputs(m_hardforks, events, blk_1r, miner_acc.get_keys(), + alice_acc.get_public_address(), MK_TEST_COINS(15), 1, TESTS_DEFAULT_FEE, tx_a_1); + CHECK_AND_ASSERT_MES(r, false, "construct_tx_with_many_outputs 1 failed"); + events.push_back(tx_a_1); + + // build tx_a_2: 16 outputs of 15 coins each to Bob for mixins + transaction tx_a_2; + r = construct_tx_with_many_outputs(m_hardforks, events, blk_1r, miner_acc.get_keys(), + bob_acc.get_public_address(), MK_TEST_COINS(15*16), 16, TESTS_DEFAULT_FEE, tx_a_2); + CHECK_AND_ASSERT_MES(r, false, "construct_tx_with_many_outputs 2 failed"); + events.push_back(tx_a_2); + + MAKE_NEXT_BLOCK_TX_LIST(events, blk_1r_1, blk_1r, miner_acc, std::list({ tx_a_1, tx_a_2 })); + + // mine a couple more blocks and rewind to generate new spendable outputs + MAKE_NEXT_BLOCK(events, blk_2, blk_1r_1, miner_acc); + REWIND_BLOCKS_N(events, blk_2r, blk_2, miner_acc, 4); + MAKE_NEXT_BLOCK(events, blk_3, blk_2r, miner_acc); + REWIND_BLOCKS_N(events, blk_4r, blk_3, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW); + + // make sure HF4 is active at 31 height + DO_CALLBACK_PARAMS(events, "check_hardfork_active", size_t{ZANO_HARDFORK_04_ZARCANUM}); + + // build tx_b with zc hf4 input, send 15 coins from miner -> Alice + std::vector sources_b; + std::vector destinations_b; + CHECK_AND_ASSERT_MES(fill_tx_sources_and_destinations( + events, blk_4r, miner_acc, alice_acc, MK_TEST_COINS(15), TESTS_DEFAULT_FEE, 2, sources_b, destinations_b), + false, "fill_tx_sources_and_destinations failed"); + currency::transaction tx_b{}; + r = construct_tx(miner_acc.get_keys(), sources_b, destinations_b, events, this, tx_b); + CHECK_AND_ASSERT_MES(r, false, "construct_tx failed"); + events.push_back(tx_b); + + // mine tx_b and rewind to unlock its outputs + MAKE_NEXT_BLOCK_TX1(events, blk_5, blk_4r, miner_acc, tx_b); + REWIND_BLOCKS_N(events, blk_5r, blk_5, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW); + + std::vector sources_c; + std::vector destinations_c; + // For input #0 use 5 mixins (old-style ring-CT allows any mixins) + // For input #1 use 16 mixins (new-style txin_zc enforces >=15 once HF4 is live) + mixins_per_input nmix_map = { {0, 5}, {1, 16} }; + CHECK_AND_ASSERT_MES(fill_tx_sources_and_destinations( + events, blk_5r, alice_acc, bob_acc, MK_TEST_COINS(29), TESTS_DEFAULT_FEE, 5, sources_c, destinations_c, + true, true, false, &nmix_map), false, "fill_tx_sources_and_destinations failed"); + currency::transaction tx_c{}; + r = construct_tx(alice_acc.get_keys(), sources_c, destinations_c, events, this, tx_c); + LOG_PRINT_GREEN("tx_c alice -> bob \n" << obj_to_json_str(tx_c), LOG_LEVEL_0); + CHECK_AND_ASSERT_MES(r, false, "construct_tx failed"); + events.push_back(tx_c); + + MAKE_NEXT_BLOCK_TX1(events, blk_6, blk_5r, miner_acc, tx_c); + + return true; +} diff --git a/tests/core_tests/tx_validation.h b/tests/core_tests/tx_validation.h index 3428d1eb..5eeea597 100644 --- a/tests/core_tests/tx_validation.h +++ b/tests/core_tests/tx_validation.h @@ -183,3 +183,11 @@ struct tx_pool_validation_and_chain_switch : public wallet_test bool generate(std::vector& events) const; bool c1(currency::core& c, size_t ev_index, const std::vector& events); }; + +struct tx_input_mixins: public test_chain_unit_enchanced +{ + tx_input_mixins(); + + bool configure_core(currency::core& c, size_t ev_index, const std::vector& events); + bool generate(std::vector& events) const; +};