// Copyright (c) 2014-2018 Zano Project // Copyright (c) 2014-2018 The Louisdor Project // Distributed under the MIT/X11 software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include "chaingen.h" #include "pos_validation.h" #include "tx_builder.h" #include "pos_block_builder.h" #include "random_helper.h" using namespace epee; using namespace crypto; using namespace currency; pos_validation::pos_validation() : m_invalid_block_index(std::numeric_limits::max()) , m_invalid_tx_index(std::numeric_limits::max()) { REGISTER_CALLBACK_METHOD(pos_validation, configure_core); REGISTER_CALLBACK_METHOD(pos_validation, mark_invalid_tx); REGISTER_CALLBACK_METHOD(pos_validation, mark_invalid_block); } bool pos_validation::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; //four blocks pc.pos_minimum_heigh = TESTS_POS_CONFIG_POS_MINIMUM_HEIGH; //four blocks c.get_blockchain_storage().set_core_runtime_config(pc); return true; } bool gen_pos_coinstake_already_spent::generate(std::vector& events) const { //test_gentime_settings s = test_generator::get_test_gentime_settings(); //s.miner_tx_max_outs = 11; // limit genesis outs for speed-up, as we going to spend all premine //test_generator::set_test_gentime_settings(s); GENERATE_ACCOUNT(preminer); GENERATE_ACCOUNT(miner); GENERATE_ACCOUNT(alice); MAKE_GENESIS_BLOCK(events, blk_0, preminer, test_core_time::get_time()); DO_CALLBACK(events, "configure_core"); MAKE_NEXT_BLOCK(events, blk_1, blk_0, miner); REWIND_BLOCKS_N(events, blk_1r, blk_1, miner, CURRENCY_MINED_MONEY_UNLOCK_WINDOW - 1); // spend all money earned from block 1 uint64_t amount = get_outs_money_amount(blk_1.miner_tx); MAKE_TX(events, tx_0, miner, alice, amount - TESTS_DEFAULT_FEE, blk_1r); MAKE_NEXT_BLOCK_TX1(events, blk_2, blk_1r, alice, tx_0); CREATE_TEST_WALLET(miner_wlt, miner, blk_0); REFRESH_TEST_WALLET_AT_GEN_TIME(events, miner_wlt, blk_2, CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 1); LOG_PRINT_L0("miner's transfers:" << ENDL << miner_wlt->dump_transfers(false)); // Legend: (n) - PoW block, [m] - PoS block // 0 1 11 12 13 <-- blockchain height (assuming CURRENCY_MINED_MONEY_UNLOCK_WINDOW == 10) // (0 )--(1 )--(1r)--(2 )--[3 ] main chain // try to make a PoS block referring to genesis' miner tx out, that is already spent // create PoS block manually block& prev_block = blk_2; crypto::hash prev_id = get_block_hash(prev_block); size_t height = get_block_height(prev_block) + 1; currency::wide_difficulty_type diff = generator.get_difficulty_for_next_block(prev_id, false); const transaction& stake = blk_1.miner_tx; crypto::public_key stake_tx_pub_key = get_tx_pub_key_from_extra(stake); size_t stake_output_idx = 0; uint64_t stake_output_gidx = 0; bool r = find_global_index_for_output(events, prev_id, stake, stake_output_idx, stake_output_gidx); CHECK_AND_ASSERT_MES(r, false, "find_global_index_for_output failed"); uint64_t stake_output_amount =boost::get( stake.vout[stake_output_idx]).amount; crypto::key_image stake_output_key_image; keypair kp; generate_key_image_helper(miner.get_keys(), stake_tx_pub_key, stake_output_idx, kp, stake_output_key_image); crypto::public_key stake_output_pubkey = boost::get(boost::get(stake.vout[stake_output_idx]).target).key; pos_block_builder pb; pb.step1_init_header(generator.get_hardforks(), height, prev_id); pb.step2_set_txs(std::vector()); pb.step3_build_stake_kernel(stake_output_amount, stake_output_gidx, stake_output_key_image, diff, prev_id, null_hash, prev_block.timestamp); pb.step4_generate_coinbase_tx(generator.get_timestamps_median(prev_id), generator.get_already_generated_coins(prev_block), alice.get_public_address()); pb.step5_sign(stake_tx_pub_key, stake_output_idx, stake_output_pubkey, miner); block blk_3 = pb.m_block; // EXPECTED: blk_2 is rejected as invalid, because coinstake refers to already spent output DO_CALLBACK(events, "mark_invalid_block"); events.push_back(blk_3); return true; } bool gen_pos_incorrect_timestamp::generate(std::vector& events) const { uint64_t genesis_ts = 1450000000; GENERATE_ACCOUNT(miner); GENERATE_ACCOUNT(alice); MAKE_GENESIS_BLOCK(events, blk_0, miner, genesis_ts); DO_CALLBACK(events, "configure_core"); REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner, BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW); // Legend: (n) - PoW block, [m] - PoS block // 0 60 61 <-- blockchain height (assuming BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW == 60) // (0 )--(0r)--[1 ] main chain /* // se uint64_t tiny_little_money = 1; MAKE_TX(events, tx_0, miner, alice, tiny_little_money, blk_0r); MAKE_NEXT_BLOCK_TX1(events, blk_1, blk_0r, miner, tx_0); */ // make a PoS block manually with incorrect timestamp crypto::hash prev_id = get_block_hash(blk_0r); size_t height = BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW + 1; currency::wide_difficulty_type diff = generator.get_difficulty_for_next_block(prev_id, false); const transaction& stake = blk_0.miner_tx; crypto::public_key stake_tx_pub_key = get_tx_pub_key_from_extra(stake); size_t stake_output_idx = 0; size_t stake_output_gidx = 0; uint64_t stake_output_amount =boost::get( stake.vout[stake_output_idx]).amount; crypto::key_image stake_output_key_image; keypair kp; generate_key_image_helper(miner.get_keys(), stake_tx_pub_key, stake_output_idx, kp, stake_output_key_image); crypto::public_key stake_output_pubkey = boost::get(boost::get(stake.vout[stake_output_idx]).target).key; pos_block_builder pb; pb.step1_init_header(generator.get_hardforks(), height, prev_id); pb.step2_set_txs(std::vector()); // use incorrect timestamp_step pb.step3_build_stake_kernel(stake_output_amount, stake_output_gidx, stake_output_key_image, diff, prev_id, null_hash, blk_0r.timestamp - 1, POS_SCAN_WINDOW, POS_SCAN_STEP - 1); pb.step4_generate_coinbase_tx(generator.get_timestamps_median(prev_id), generator.get_already_generated_coins(blk_0r), alice.get_public_address()); pb.step5_sign(stake_tx_pub_key, stake_output_idx, stake_output_pubkey, miner); block blk_1 = pb.m_block; // EXPECTED: blk_1 is rejected as invalid, because it's timestamps % POS_SCAN_STEP != 0 DO_CALLBACK(events, "mark_invalid_block"); events.push_back(blk_1); // shift core time a little uint64_t ts = blk_0r.timestamp + 60; events.push_back(event_core_time(ts)); // Now try PoS timestamp window boundaries. pb.clear(); pb.step1_init_header(generator.get_hardforks(), height, prev_id); pb.step2_set_txs(std::vector()); // move timestamp to the future pb.step3_build_stake_kernel(stake_output_amount, stake_output_gidx, stake_output_key_image, diff, prev_id, null_hash, ts + CURRENCY_POS_BLOCK_FUTURE_TIME_LIMIT + 1, POS_SCAN_WINDOW, POS_SCAN_STEP); pb.step4_generate_coinbase_tx(generator.get_timestamps_median(prev_id), generator.get_already_generated_coins(blk_0r), alice.get_public_address()); pb.step5_sign(stake_tx_pub_key, stake_output_idx, stake_output_pubkey, miner); block blk_2 = pb.m_block; // EXPECTED: blk_2 is rejected as invalid, because its timestamp is beyond upper limit DO_CALLBACK(events, "mark_invalid_block"); events.push_back(blk_2); // lower limit pb.clear(); pb.step1_init_header(generator.get_hardforks(), height, prev_id); pb.step2_set_txs(std::vector()); // move timestamp to the future pb.step3_build_stake_kernel(stake_output_amount, stake_output_gidx, stake_output_key_image, diff, prev_id, null_hash, genesis_ts - POS_SCAN_WINDOW, POS_SCAN_WINDOW, POS_SCAN_STEP); pb.step4_generate_coinbase_tx(generator.get_timestamps_median(prev_id), generator.get_already_generated_coins(blk_0r), alice.get_public_address()); pb.step5_sign(stake_tx_pub_key, stake_output_idx, stake_output_pubkey, miner); block blk_3 = pb.m_block; // EXPECTED: blk_3 is rejected as invalid, because its timestamp is beyond lower limit DO_CALLBACK(events, "mark_invalid_block"); events.push_back(blk_3); return true; } //------------------------------------------------------------------ gen_pos_too_early_pos_block::gen_pos_too_early_pos_block() : m_pos_min_height(CURRENCY_MINED_MONEY_UNLOCK_WINDOW + TESTS_POS_CONFIG_POS_MINIMUM_HEIGH) { REGISTER_CALLBACK_METHOD(gen_pos_too_early_pos_block, configure_core); } bool gen_pos_too_early_pos_block::generate(std::vector& events) const { uint64_t ts = time(NULL); GENERATE_ACCOUNT(miner); MAKE_GENESIS_BLOCK(events, blk_0, miner, ts); DO_CALLBACK(events, "configure_core"); REWIND_BLOCKS_N(events, blk_0r, blk_0, miner, m_pos_min_height - 2); // Legend: (n) - PoW block, [m] - PoS block // 0 N N+1 <-- blockchain height (N= CURRENCY_MINED_MONEY_UNLOCK_WINDOW + TESTS_POS_CONFIG_POS_MINIMUM_HEIGH - 2, now is 12) // (0 )--(0r)--[1 ] main chain // EXPECTED: blk_1 is rejected as invalid, because PoS blocks are not allowed on that height DO_CALLBACK(events, "mark_invalid_block"); MAKE_NEXT_POS_BLOCK(events, blk_1, blk_0r, miner, std::list(1, miner)); return true; } bool gen_pos_too_early_pos_block::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 = m_pos_min_height; c.get_blockchain_storage().set_core_runtime_config(pc); return true; } //------------------------------------------------------------------ bool gen_pos_extra_nonce::configure_core(currency::core& c, size_t ev_index, const std::vector&) { 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 = 0; pc.hard_forks = m_hardforks; c.get_blockchain_storage().set_core_runtime_config(pc); return true; } gen_pos_extra_nonce::gen_pos_extra_nonce() { REGISTER_CALLBACK_METHOD(gen_pos_extra_nonce, configure_core); REGISTER_CALLBACK_METHOD(gen_pos_extra_nonce, request_pow_template_with_nonce); REGISTER_CALLBACK_METHOD(gen_pos_extra_nonce, check_pow_nonce); REGISTER_CALLBACK_METHOD(gen_pos_extra_nonce, check_pos_nonce); } // Test: verify custom extra_nonce in blocks and templates /* * Scenarios: * 1. PoW block contains m_pow_nonce * 2. PoS block contains m_pos_nonce * 3. PoW mining template contains m_pow_template_nonce */ bool gen_pos_extra_nonce::generate(std::vector& events) const { GENERATE_ACCOUNT(miner); GENERATE_ACCOUNT(alice); m_accounts.push_back(miner); m_accounts.push_back(alice); m_pow_nonce = currency::blobdata(254, 'w'); m_pos_nonce = currency::blobdata(255, 's'); m_pow_template_nonce = "POW_TEMPLATE123"; uint64_t ts = test_core_time::get_time(); MAKE_GENESIS_BLOCK(events, blk_0, miner, ts); DO_CALLBACK(events, "configure_core"); MAKE_NEXT_BLOCK(events, blk_1, blk_0, miner); block blk_2 = AUTO_VAL_INIT(blk_2); ts = blk_2.timestamp + DIFFICULTY_BLOCKS_ESTIMATE_TIMESPAN / 2; // to increase main chain difficulty bool r = generator.construct_block_manually(blk_2, blk_1, miner, test_generator::bf_timestamp, 0, 0, ts, crypto::hash(), 1, transaction(), std::vector(), 0, m_pow_nonce); CHECK_AND_ASSERT_MES(r, false, "construct_block_manually failed"); events.push_back(blk_2); DO_CALLBACK(events, "check_pow_nonce"); REWIND_BLOCKS(events, blk_0r, blk_2, miner); // setup params for PoS const currency::transaction& stake = blk_2.miner_tx; currency::block new_pos_block; bool ok = generate_pos_block_with_extra_nonce(generator, events, miner, alice, blk_0r, stake, m_pos_nonce, new_pos_block); CHECK_AND_ASSERT_MES(ok, false, "generate_pos_block_with_extra_nonce failed"); events.push_back(new_pos_block); DO_CALLBACK(events, "check_pos_nonce"); DO_CALLBACK(events, "request_pow_template_with_nonce"); return true; } bool gen_pos_extra_nonce::request_pow_template_with_nonce(currency::core& c, size_t ev_index, const std::vector& events) { block bl; wide_difficulty_type diff; uint64_t height; bool ok = c.get_block_template(bl, m_accounts[0].get_public_address(), m_accounts[0].get_public_address(), diff, height, m_pow_template_nonce); CHECK_AND_ASSERT_MES(ok, false, "get_block_template failed"); CHECK_AND_ASSERT_MES(has_extra_nonce(bl, m_pow_template_nonce), false, "PoW extra_nonce not found"); return true; } bool gen_pos_extra_nonce::check_pow_nonce(currency::core& c, size_t ev_index, const std::vector& events) { block top; bool ok = c.get_blockchain_storage().get_top_block(top); CHECK_AND_ASSERT_MES(ok, false, "get_top_block failed"); CHECK_AND_ASSERT_MES(has_extra_nonce(top, m_pow_nonce), false, "PoW extra_nonce not found"); return true; } bool gen_pos_extra_nonce::check_pos_nonce(currency::core& c, size_t ev_index, const std::vector& events) { block top; bool ok = c.get_blockchain_storage().get_top_block(top); CHECK_AND_ASSERT_MES(ok, false, "get_top_block failed"); CHECK_AND_ASSERT_MES(has_extra_nonce(top, m_pos_nonce), false, "PoS extra_nonce not found"); return true; } bool gen_pos_extra_nonce::has_extra_nonce(currency::block& blk, const std::string& expected_nonce) { if (auto const* ud = get_type_in_variant_container(blk.miner_tx.extra)) { LOG_PRINT_L0("Found extra nonce: '" << ud->buff << "' expected: '" << expected_nonce << "'"); return ud->buff == expected_nonce; } return false; } //------------------------------------------------------------------ gen_pos_min_allowed_height::gen_pos_min_allowed_height() { REGISTER_CALLBACK_METHOD(gen_pos_min_allowed_height, configure_core); } bool gen_pos_min_allowed_height::generate(std::vector& events) const { GENERATE_ACCOUNT(miner); GENERATE_ACCOUNT(alice); MAKE_GENESIS_BLOCK(events, blk_0, miner, test_core_time::get_time()); DO_CALLBACK(events, "configure_core"); REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, alice, CURRENCY_MINED_MONEY_UNLOCK_WINDOW); // Legend: (n) - PoW block, [m] - PoS block // 0 N N+1 <-- blockchain height (assuming CURRENCY_MINED_MONEY_UNLOCK_WINDOW == N) // (0 )--(0r)--[1 ] main chain MAKE_TX(events, tx_1, alice, miner, MK_TEST_COINS(1), blk_0r); crypto::hash prev_id = get_block_hash(blk_0r); size_t height = CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 1; currency::wide_difficulty_type diff = generator.get_difficulty_for_next_block(prev_id, false); const transaction& stake = blk_0.miner_tx; crypto::public_key stake_tx_pub_key = get_tx_pub_key_from_extra(stake); size_t stake_output_idx = 0; size_t stake_output_gidx = 0; uint64_t stake_output_amount =boost::get( stake.vout[stake_output_idx]).amount; crypto::key_image stake_output_key_image; keypair kp; generate_key_image_helper(miner.get_keys(), stake_tx_pub_key, stake_output_idx, kp, stake_output_key_image); crypto::public_key stake_output_pubkey = boost::get(boost::get(stake.vout[stake_output_idx]).target).key; pos_block_builder pb; pb.step1_init_header(generator.get_hardforks(), height, prev_id); pb.step2_set_txs(std::vector(1, tx_1)); pb.step3_build_stake_kernel(stake_output_amount, stake_output_gidx, stake_output_key_image, diff, prev_id, null_hash, blk_0r.timestamp); pb.step4_generate_coinbase_tx(generator.get_timestamps_median(prev_id), generator.get_already_generated_coins(blk_0r), alice.get_public_address()); pb.step5_sign(stake_tx_pub_key, stake_output_idx, stake_output_pubkey, miner); block blk_1 = pb.m_block; // EXPECTED: blk_1 is accepted events.push_back(blk_1); return true; } bool gen_pos_min_allowed_height::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; //four blocks pc.pos_minimum_heigh = CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 1; c.get_blockchain_storage().set_core_runtime_config(pc); return true; } //------------------------------------------------------------------ bool gen_pos_invalid_coinbase::generate(std::vector& events) const { // Test idea: make a PoS block with zero signature in miner_tx (exploits a bug). GENERATE_ACCOUNT(miner_acc); GENERATE_ACCOUNT(alice_acc); MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, test_core_time::get_time()); DO_CALLBACK(events, "configure_core"); REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW); // create PoS block manually const block& prev_block = blk_0r; crypto::hash prev_id = get_block_hash(prev_block); size_t height = CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 1; currency::wide_difficulty_type diff = generator.get_difficulty_for_next_block(prev_id, false); const transaction& stake = blk_0.miner_tx; crypto::public_key stake_tx_pub_key = get_tx_pub_key_from_extra(stake); size_t stake_output_idx = 0; size_t stake_output_gidx = 0; uint64_t stake_output_amount =boost::get( stake.vout[stake_output_idx]).amount; crypto::key_image stake_output_key_image; keypair kp; generate_key_image_helper(miner_acc.get_keys(), stake_tx_pub_key, stake_output_idx, kp, stake_output_key_image); crypto::public_key stake_output_pubkey = boost::get(boost::get(stake.vout[stake_output_idx]).target).key; pos_block_builder pb; pb.step1_init_header(generator.get_hardforks(), height, prev_id); pb.step2_set_txs(std::vector()); pb.step3_build_stake_kernel(stake_output_amount, stake_output_gidx, stake_output_key_image, diff, prev_id, null_hash, prev_block.timestamp); pb.step4_generate_coinbase_tx(generator.get_timestamps_median(prev_id), generator.get_already_generated_coins(prev_block), alice_acc.get_public_address()); pb.step5_sign(stake_tx_pub_key, stake_output_idx, stake_output_pubkey, miner_acc); pb.m_block.miner_tx.signatures.clear(); // remove signatures block blk_1 = pb.m_block; // EXPECTED: blk_1 is rejected as invalid, because coinbase cointains no signatures DO_CALLBACK(events, "mark_invalid_block"); events.push_back(blk_1); return true; } //------------------------------------------------------------------ void decompose_amount_into_exact_number_of_pos_entries(uint64_t amount, size_t pos_entries_count, std::vector &pos_amounts) { std::list pos_amounts_list; // decompose algorithm: we need exactly 'pos_entries_count' entries of power-10 which will give 'amount' in sum auto f = [&pos_amounts_list](uint64_t a){ pos_amounts_list.push_back(a); }; decompose_amount_into_digits(amount, DEFAULT_DUST_THRESHOLD, f, f); // get some initial decomposition while (pos_amounts_list.size() < pos_entries_count) { for (auto it = pos_amounts_list.begin(); it != pos_amounts_list.end() && pos_amounts_list.size() < pos_entries_count; /* nothing */) { if (*it >= 2 * TESTS_DEFAULT_FEE) { // each iteration pops one element 'a' and pushes two elements: c1 and c2, so that a == c1 + c2 (sum is invariant) uint64_t a = *it; // remove zeros form the tail of 'a': a = 1230000 -> c = 123 * pow = 10000 uint64_t c = a, pow = 1; for(;;) { uint64_t tc = c / 10, tpow = pow * 10; if (tc * tpow != a) break; c = tc; pow = tpow; } if (c == 1) { c = 10; pow /= 10; } // randomly split 'a' into sum of c1 and c2 // c in [2; 10], pow in [1...10^^n] uint64_t c1 = random_in_range(1, c - 1); // c1: [1; c-1] uint64_t c2 = c - c1; // c2: [c1; c], c1 + c2 == c pos_amounts_list.push_front(c1 * pow); pos_amounts_list.push_front(c2 * pow); it = pos_amounts_list.erase(it); continue; } ++it; } } // random shuffle pos amounts to get rid of any kind of order pos_amounts.assign(pos_amounts_list.begin(), pos_amounts_list.end()); std::shuffle(pos_amounts.begin(), pos_amounts.end(), crypto::uniform_random_bit_generator()); } bool populate_wallet_with_stake_coins(std::shared_ptr w, std::shared_ptr money_source_w, size_t pos_entries_count, uint64_t amount, const std::vector& pos_amounts) { #define WALLET_TRY_CATCH(statement) \ try \ { \ statement; \ } \ catch (std::exception& e) \ { \ LOG_ERROR("Wallet operation failed: " << e.what()); \ return false; \ } LOG_PRINT_L0("populate_wallet_with_stake_coins: amount: " << print_money(amount) << ", pos_entries_count: " << pos_entries_count); // make sure w is empty w->refresh(); uint64_t balance_unlocked = 0; uint64_t balance = w->balance(balance_unlocked); CHECK_AND_ASSERT_MES(balance == balance_unlocked, false, "WARNING: balance is " << balance << " NOT EQUAL TO unlocked balance, which is " << balance_unlocked); if (balance_unlocked > TESTS_DEFAULT_FEE) WALLET_TRY_CATCH(w->transfer(balance_unlocked, money_source_w->get_account().get_public_address())); uint64_t sum = 0; for(auto a : pos_amounts) sum += a; CHECK_AND_ASSERT_MES(pos_amounts.size() == pos_entries_count && sum == amount, false, "Failed to decompose " << print_money(amount) << " into " << pos_entries_count << " entries: pos_amounts.size is " << pos_amounts.size() << ", sum is " << sum); // populate current wallet with pos entries from scratch money_source_w->refresh(); balance = money_source_w->balance(balance_unlocked); CHECK_AND_ASSERT_MES(balance_unlocked > amount + TESTS_DEFAULT_FEE * pos_entries_count, false, "source wallet has not enough money: balance_unlocked: " << balance_unlocked << ", balance: " << balance << ", required amount: " << amount + TESTS_DEFAULT_FEE * pos_entries_count); const account_public_address& wallet_address = w->get_account().get_public_address(); std::vector destinations; destinations.reserve(pos_amounts.size()); for(auto pos_amount : pos_amounts) destinations.push_back(tx_destination_entry(pos_amount, wallet_address)); WALLET_TRY_CATCH(money_source_w->transfer(destinations, 0, 0, TESTS_DEFAULT_FEE, empty_extra, empty_attachment)); return true; #undef WALLET_TRY_CATCH } bool pos_wallet_minting_same_amount_diff_outs::prepare_wallets_0(currency::core& c, const std::vector& events, std::vector &minting_wallets, std::vector& minting_wallets_order) { // Prepare equal wallets: same amount, same transfers (count and amount), same pos entries // This is to test evenness of simulation environment and the stuff beyound wallet pos entries. bool r = false; block b = AUTO_VAL_INIT(b); std::shared_ptr alice_wlt = init_playtime_test_wallet(events, c, ALICE_ACC_IDX); // Alice is preminer size_t pos_entries_count = 990; std::vector pos_amounts; // use the same stake decomposition for all wallets decompose_amount_into_exact_number_of_pos_entries(m_wallet_stake_amount, pos_entries_count, pos_amounts); std::vector first_wallet_pos_entries; // populate minting wallets with money for (size_t i = 0; i < m_minting_wallets_count; ++i) { minting_wallets_order[i] = i; auto& w = minting_wallets[i]; w.w = init_playtime_test_wallet(events, c, ALICE_ACC_IDX + 1 + i); //w.pos_entries_count = random_in_range(m_pos_entries_count_min, m_pos_entries_count_max); w.pos_entries_count = pos_entries_count; r = populate_wallet_with_stake_coins(w.w, alice_wlt, w.pos_entries_count, m_wallet_stake_amount, pos_amounts); CHECK_AND_ASSERT_MES(r, false, "populate_wallet_with_stake_coins failed"); r = mine_next_pow_blocks_in_playtime(alice_wlt->get_account().get_public_address(), c, WALLET_DEFAULT_TX_SPENDABLE_AGE + 1); // to preventing run out of the money for Alice CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_blocks_in_playtime failed"); c.get_blockchain_storage().get_top_block(b); uint64_t ts = b.timestamp; test_core_time::adjust(ts); // check pos entries actually available w.w->refresh(); std::vector pos_entries; r = w.w->get_pos_entries(pos_entries); CHECK_AND_ASSERT_MES(r, false, "get_pos_entries failed"); CHECK_AND_ASSERT_MES(pos_entries.size() == w.pos_entries_count, false, "incorrect pos entries count in the wallet: " << pos_entries.size() << ", expected: " << w.pos_entries_count); uint64_t balance_unlocked = 0; w.w->balance(balance_unlocked); CHECK_AND_ASSERT_MES(balance_unlocked == m_wallet_stake_amount, false, "incorrect wallet unlocked balance: " << print_money(balance_unlocked) << ", expected: " << print_money(m_wallet_stake_amount)); // make sure all wallets has the same distribution of pos_entries if (i == 0) { first_wallet_pos_entries = pos_entries; } else { for(size_t i = 0; i < pos_amounts.size(); ++i) { CHECK_AND_ASSERT_MES(pos_entries[i].amount == first_wallet_pos_entries[i].amount, false, "Wallet pos entry #" << i << " has incorrect amount: " << print_money(pos_entries[i].amount) << ", expected (as in 1st wallet): " << print_money(first_wallet_pos_entries[i].amount)); } } } return true; } bool pos_wallet_minting_same_amount_diff_outs::prepare_wallets_1(currency::core& c, const std::vector& events, std::vector &minting_wallets, std::vector& minting_wallets_order) { // prepare five wallets with the same total amount but different pos entries distribution to test how it influence minting efficiency size_t pos_entries_count_values[] = {100, 1000, 300, 990, 2000}; CHECK_AND_ASSERT_MES(m_minting_wallets_count == (sizeof pos_entries_count_values / sizeof pos_entries_count_values[0]), false, "sizeof pos_entries_count_values != m_minting_wallets_count"); bool r = false; block b = AUTO_VAL_INIT(b); std::shared_ptr alice_wlt = init_playtime_test_wallet(events, c, ALICE_ACC_IDX); // Alice is preminer // populate minting wallets with money for (size_t i = 0; i < m_minting_wallets_count; ++i) { minting_wallets_order[i] = i; auto& w = minting_wallets[i]; w.w = init_playtime_test_wallet(events, c, ALICE_ACC_IDX + 1 + i); //w.pos_entries_count = random_in_range(m_pos_entries_count_min, m_pos_entries_count_max); w.pos_entries_count = pos_entries_count_values[i % (sizeof pos_entries_count_values / sizeof pos_entries_count_values[0])]; std::vector pos_amounts; decompose_amount_into_exact_number_of_pos_entries(m_wallet_stake_amount, w.pos_entries_count, pos_amounts); r = populate_wallet_with_stake_coins(w.w, alice_wlt, w.pos_entries_count, m_wallet_stake_amount, pos_amounts); CHECK_AND_ASSERT_MES(r, false, "populate_wallet_with_stake_coins failed"); r = mine_next_pow_blocks_in_playtime(alice_wlt->get_account().get_public_address(), c, WALLET_DEFAULT_TX_SPENDABLE_AGE + 1); // to preventing run out of the money for Alice CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_blocks_in_playtime failed"); c.get_blockchain_storage().get_top_block(b); uint64_t ts = b.timestamp; test_core_time::adjust(ts); // check pos entries actually available w.w->refresh(); size_t pos_entries_count = w.w->get_pos_entries_count(); CHECK_AND_ASSERT_MES(pos_entries_count == w.pos_entries_count, false, "incorrect pos entries count in the wallet: " << pos_entries_count << ", expected: " << w.pos_entries_count); uint64_t balance_unlocked = 0; w.w->balance(balance_unlocked); CHECK_AND_ASSERT_MES(balance_unlocked == m_wallet_stake_amount, false, "incorrect wallet unlocked balance: " << print_money(balance_unlocked) << ", expected: " << print_money(m_wallet_stake_amount)); } return true; } pos_wallet_minting_same_amount_diff_outs::pos_wallet_minting_same_amount_diff_outs() { REGISTER_CALLBACK_METHOD(pos_wallet_minting_same_amount_diff_outs, c1); m_minting_wallets_count = 5; m_wallet_stake_amount = 1000 * COIN; } bool pos_wallet_minting_same_amount_diff_outs::generate(std::vector& events) const { GENERATE_ACCOUNT(miner_acc); GENERATE_ACCOUNT(alice_acc); m_accounts.resize(ALICE_ACC_IDX + 1 + m_minting_wallets_count); m_accounts[MINER_ACC_IDX] = miner_acc; m_accounts[ALICE_ACC_IDX] = alice_acc; for (size_t i = 0; i < m_minting_wallets_count; ++i) m_accounts[ALICE_ACC_IDX + 1 + i].generate(); MAKE_GENESIS_BLOCK(events, blk_0, alice_acc, test_core_time::get_time()); // Alice is a preminer! REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW); DO_CALLBACK(events, "c1"); return true; } bool pos_wallet_minting_same_amount_diff_outs::c1(currency::core& c, size_t ev_index, const std::vector& events) { // Test idea: few wallets with the same total amount but different transfers are mining simultaneously. // All generated money is calculated and goes to Miner, so wallets' balances are constant. // Generated amounts per each wallet should be more or less the same, as far they have the same stakes. uint64_t ts = test_core_time::get_time(); bool r = false; block b = AUTO_VAL_INIT(b); // // 1/2 prepare wallets // std::vector minting_wallets_order(m_minting_wallets_count); // to iterate wallets in random order while minting std::vector minting_wallets(m_minting_wallets_count); r = prepare_wallets_0(c, events, minting_wallets, minting_wallets_order); CHECK_AND_ASSERT_MES(r, false, "prepare wallets failed"); // // 2/2 simulate the mining process // uint64_t pos_target = DIFFICULTY_POS_TARGET; uint64_t pow_target = DIFFICULTY_POW_TARGET; uint64_t last_pow_block_ts = ts - pow_target; //uint64_t last_pos_block_ts = ts - pos_target; // block_header::ts //uint64_t last_pos_block_ts_real = ts - pos_target; // real time at the moment of successfull minting wide_difficulty_type last_pos_diff = 0; size_t pos_blocks = 0; uint64_t total_minted_coins = 0; #define LOG1_FILENAME "pos_wallet_minting_same_amount_diff_outs.log" // use custom log to store data sheet-friendly // print header to the log std::stringstream ss; ss << "\t" << "height" << "\t" << "pos %" << "\t" << "mnt.coins/pos blks" << "\t" << "last_pos_diff" << "\t" << "stake_to_diff_coeff" << "\t" << "pow_seq_factor" << "\t" << "pos_seq_factor"; for (size_t w_idx = 0; w_idx < m_minting_wallets_count; ++w_idx) ss << "\tw#" << w_idx << "_" << minting_wallets[w_idx].pos_entries_count; LOG_PRINT2(LOG1_FILENAME, ss.str(), LOG_LEVEL_0); for(size_t n = 0; n < 30000; ++n) { // log status c.get_blockchain_storage().get_top_block(b); size_t height = get_block_height(b); size_t pow_seq_factor = c.get_blockchain_storage().get_current_sequence_factor(false); size_t pos_seq_factor = c.get_blockchain_storage().get_current_sequence_factor(true); double pos_total_stake_to_diff_coeff = static_cast(last_pos_diff / COIN) / static_cast(m_minting_wallets_count * m_wallet_stake_amount / COIN); float pos_blocks_proportion = static_cast(pos_blocks) / height; uint64_t minted_coins_per_pos_block = total_minted_coins / height; ss.str(""); // <-- just a wired way to clear stringstream, have a good day! ss << "\t" << height << "\t" << pos_blocks_proportion << "\t" << print_money(minted_coins_per_pos_block) << "\t" << last_pos_diff << "\t" << pos_total_stake_to_diff_coeff << "\t" << pow_seq_factor << "\t" << pos_seq_factor; for (size_t w_idx = 0; w_idx < m_minting_wallets_count; ++w_idx) ss << "\t" << print_money(minting_wallets[w_idx].mined_coins); LOG_PRINT2(LOG1_FILENAME, ss.str(), LOG_LEVEL_0); if (n % 250 == 0) dump_wallets_entries(minting_wallets); // pow uint64_t ideal_next_pow_block_ts = last_pow_block_ts + pow_target; if (ts < ideal_next_pow_block_ts) ts = ideal_next_pow_block_ts; // "wait" until ideal_next_pow_block_ts if it was not already happened (fast forward but don't wayback the time) test_core_time::adjust(ts); size_t tx_count_before = c.get_pool_transactions_count(); r = mine_next_pow_block_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c); CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_block_in_playtime failed"); CHECK_AND_ASSERT_MES(tx_count_before == 0 || c.get_pool_transactions_count() < tx_count_before, false, "invalid number of txs in tx pool: " << c.get_pool_transactions_count() << ", was: " << tx_count_before); last_pow_block_ts = ts; // pos uint64_t pos_ts_upper_bound = ts + pos_target / 2; while(ts <= pos_ts_upper_bound) { // every POS_SCAN_STEP all wallets tries to mint a PoS block simultaneously std::shuffle(minting_wallets_order.begin(), minting_wallets_order.end(), crypto::uniform_random_bit_generator()); // shuffle to iterate in random order bool a_block_was_minted = true; do { a_block_was_minted = false; for(size_t i = 0; i < m_minting_wallets_count; ++i) { size_t w_idx = minting_wallets_order[i]; LOG_PRINT_L0("Minting with wallet #" << w_idx); auto& current_wallet = minting_wallets[w_idx]; current_wallet.w->refresh(); if (n % 10 == 0) // check balances every 10 PoW blocks in a sake of speed { uint64_t balance = current_wallet.w->balance(); CHECK_AND_ASSERT_MES(balance == m_wallet_stake_amount, false, "Wallet #" << w_idx << " has incorrect balance: " << print_money(balance) << ", expected: " << print_money(m_wallet_stake_amount)); } last_pos_diff = c.get_blockchain_storage().get_next_diff_conditional(true); uint64_t bc_height_before = c.get_blockchain_storage().get_current_blockchain_size(); size_t pos_entries_count = 0; if (mine_next_pos_block_in_playtime_with_wallet(*current_wallet.w.get(), m_accounts[MINER_ACC_IDX].get_public_address(), pos_entries_count)) { ++pos_blocks; a_block_was_minted = true; height = c.get_blockchain_storage().get_current_blockchain_size(); CHECK_AND_ASSERT_MES(bc_height_before < height, false, "didn't mint a block!"); LOG_PRINT_GREEN(">>>>>>>>>>>>>> Minted a PoS block by wallet #" << w_idx << " at height " << height << " and time " << ts << " with difficuly: " << last_pos_diff << " using " << pos_entries_count << " pos entries", LOG_LEVEL_0); CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "invalid number of txs in tx pool: " << c.get_pool_transactions_count()); c.get_blockchain_storage().get_top_block(b); //last_pos_block_ts = b.timestamp; //last_pos_block_ts_real = ts; CHECK_AND_ASSERT_MES(is_pos_block(b), false, "Top block is not a PoS block!"); uint64_t generated_coins = get_outs_money_amount(b.miner_tx) - boost::get(b.miner_tx.vin[1]).amount; current_wallet.mined_coins += generated_coins; total_minted_coins += generated_coins; } } } while(a_block_was_minted); ts += POS_SCAN_STEP; test_core_time::adjust(ts); } } // Todo: add generated money comparation return true; #undef LOG1_FILENAME } void pos_wallet_minting_same_amount_diff_outs::dump_wallets_entries(const std::vector& minting_wallets) { #define LOG2_FILENAME "pos_wallet_minting_same_amount_diff_outs.wallets_dumps.log" LOG_PRINT2(LOG2_FILENAME, ENDL << ENDL << ENDL << ENDL << ENDL << ENDL, LOG_LEVEL_0); for (size_t i = 0; i < minting_wallets.size(); ++i) { LOG_PRINT2(LOG2_FILENAME, "wallet #" << i << ":" << ENDL << minting_wallets[i].w->dump_transfers() << ENDL, LOG_LEVEL_0); } #undef LOG2_FILENAME } //------------------------------------------------------------------ pos_wallet_big_block_test::pos_wallet_big_block_test() { REGISTER_CALLBACK_METHOD(pos_wallet_big_block_test, c1); REGISTER_CALLBACK_METHOD(pos_wallet_big_block_test, configure_core); } bool pos_wallet_big_block_test::generate(std::vector& events) const { // Test idea: check PoS mining via wallet for block size greather than median (of sizes of last N block) GENERATE_ACCOUNT(preminer_acc); GENERATE_ACCOUNT(miner_acc); m_accounts.push_back(miner_acc); MAKE_GENESIS_BLOCK(events, blk_0, preminer_acc, test_core_time::get_time()); DO_CALLBACK(events, "configure_core"); REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 3); // make a couple of huge txs bool r = false; size_t padding_size = CURRENCY_MAX_TRANSACTION_BLOB_SIZE * 5 / 8; // to get over one median (of sizes of last N blocks) but not to exceed 2 medians extra_padding padding = AUTO_VAL_INIT(padding); padding.buff.resize(padding_size); std::vector extra({ padding }); std::vector sources_1, sources_2; r = fill_tx_sources(sources_1, events, blk_0r, miner_acc.get_keys(), MK_TEST_COINS(3), 0); CHECK_AND_ASSERT_MES(r, false, "fill_tx_sources failed"); r = fill_tx_sources(sources_2, events, blk_0r, miner_acc.get_keys(), MK_TEST_COINS(3), 0, sources_1); CHECK_AND_ASSERT_MES(r, false, "fill_tx_sources failed"); std::vector destinations({ tx_destination_entry(MK_TEST_COINS(1), miner_acc.get_public_address())}); // yep, it will be bigger fee than usual, don't mind crypto::secret_key stub; transaction tx_1 = AUTO_VAL_INIT(tx_1); r = construct_tx(miner_acc.get_keys(), sources_1, destinations, extra, empty_attachment, tx_1, get_tx_version_from_events(events), stub, 0); CHECK_AND_ASSERT_MES(r, false, "construct_tx failed"); size_t tx_size = get_object_blobsize(tx_1); CHECK_AND_ASSERT_MES(tx_size > padding_size, false, "Tx size is: " << tx_size << ", expected to be bigger than: " << padding_size); events.push_back(tx_1); // push it to the pool transaction tx_2 = AUTO_VAL_INIT(tx_2); r = construct_tx(miner_acc.get_keys(), sources_2, destinations, extra, empty_attachment, tx_2, get_tx_version_from_events(events), stub, 0); CHECK_AND_ASSERT_MES(r, false, "construct_tx failed"); tx_size = get_object_blobsize(tx_2); CHECK_AND_ASSERT_MES(tx_size > padding_size, false, "Tx size is: " << tx_size << ", expected to be bigger than: " << padding_size); events.push_back(tx_2); // push it to the pool DO_CALLBACK(events, "c1"); return true; } bool pos_wallet_big_block_test::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; //four blocks pc.pos_minimum_heigh = TESTS_POS_CONFIG_POS_MINIMUM_HEIGH; //four blocks c.get_blockchain_storage().set_core_runtime_config(pc); return true; } bool pos_wallet_big_block_test::c1(currency::core& c, size_t ev_index, const std::vector& events) { bool r = false, stub_bool; std::shared_ptr miner_wlt = init_playtime_test_wallet(events, c, MINER_ACC_IDX); miner_wlt->refresh(); miner_wlt->scan_tx_pool(stub_bool); LOG_PRINT_L0("miner transfers:" << ENDL << miner_wlt->dump_transfers(false)); CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 2, false, "Incorrect number of txs in the pool: " << c.get_pool_transactions_count()); try { r = miner_wlt->try_mint_pos(); } catch (std::exception& e) { LOG_ERROR("Got exception in try_mint_pos(): " << e.what()); return false; } CHECK_AND_ASSERT_MES(r, false, "try_mint_pos failed"); // make sure the tx has gone CHECK_AND_ASSERT_MES(c.get_pool_transactions_count() == 0, false, "Incorrect number of txs in the pool: " << c.get_pool_transactions_count()); // check top block - should be PoS with certain size block b = AUTO_VAL_INIT(b); r = c.get_blockchain_storage().get_top_block(b); CHECK_AND_ASSERT_MES(r, false, "get_top_block failed"); CHECK_AND_ASSERT_MES(is_pos_block(b), false, "top block is not PoS"); size_t block_size = get_object_blobsize(b.miner_tx); for (const crypto::hash& h : b.tx_hashes) { transaction t = AUTO_VAL_INIT(t); r = c.get_transaction(h, t); CHECK_AND_ASSERT_MES(r, false, "get_transaction failed"); block_size += get_object_blobsize(t); } uint64_t block_size_median = c.get_blockchain_storage().get_current_comulative_blocksize_limit() / 2; CHECK_AND_ASSERT_MES(block_size_median < block_size && block_size < 2 * block_size_median, false, "Incorrect PoS block size: " << block_size << ", expected to be between median and 2 medinas: " << block_size_median); return true; } //------------------------------------------------------------------ pos_altblocks_validation::pos_altblocks_validation() { //test_chain_unit_base::set_hardforks_for_old_tests(); } bool pos_altblocks_validation::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 = 1; pc.pos_minimum_heigh = 1; c.get_blockchain_storage().set_core_runtime_config(pc); return true; } bool pos_altblocks_validation::generate(std::vector& events) const { bool r = false; m_accounts.resize(TOTAL_ACCS_COUNT); account_base& miner_acc = m_accounts[MINER_ACC_IDX]; miner_acc.generate(); account_base& alice_acc = m_accounts[ALICE_ACC_IDX]; alice_acc.generate(); std::list miner_acc_lst(1, miner_acc); MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, test_core_time::get_time()); DO_CALLBACK(events, "configure_core"); REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW); MAKE_NEXT_BLOCK(events, blk_1, blk_0r, alice_acc); REWIND_BLOCKS_N_WITH_TIME(events, blk_1r, blk_1, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW); bool HF4_active = m_hardforks.is_hardfork_active_for_height(ZANO_HARDFORK_04_ZARCANUM, get_block_height(blk_1r) + 1); MAKE_NEXT_BLOCK(events, blk_2, blk_1r, miner_acc); const transaction& stake_tx = blk_1.miner_tx; uint64_t stake_tx_out_id = 0; uint64_t alice_money = decode_native_output_amount_or_throw(alice_acc, blk_1.miner_tx, stake_tx_out_id); //// select stake_tx_out_id as an output with the biggest amount //for (size_t i = 1; i < stake_tx.vout.size(); ++i) //{ // if (boost::get(stake_tx.vout[i]).amount > boost::get( stake_tx.vout[stake_tx_out_id]).amount) // stake_tx_out_id = i; //} MAKE_TX_FEE(events, tx_0, alice_acc, alice_acc, alice_money - TESTS_DEFAULT_FEE * 17, TESTS_DEFAULT_FEE * 17, blk_2); // tx_0 transfers all Alice's money, so it effectevily spends all outputs in stake_ts, make sure it does //CHECK_AND_ASSERT_MES(tx_0.vin.size() == stake_tx.vout.size(), false, "probably, tx_0 doesn't spend all Alice's money as expected, tx_0.vin.size()=" << tx_0.vin.size() << ", stake_tx.vout.size()=" << stake_tx.vout.size()); MAKE_NEXT_BLOCK_TX1(events, blk_3, blk_2, miner_acc, tx_0); // remember some tx_0's output info for further reference size_t tx_0_some_output_idx = 0; uint64_t tx_0_some_output_gindex = UINT64_MAX; r = find_global_index_for_output(events, get_block_hash(blk_3), tx_0, tx_0_some_output_idx, tx_0_some_output_gindex); CHECK_AND_ASSERT_MES(r, false, "find_global_index_for_output failed"); REWIND_BLOCKS_N_WITH_TIME(events, blk_3r, blk_3, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW); MAKE_NEXT_POS_BLOCK(events, blk_4, blk_3r, miner_acc, miner_acc_lst); MAKE_NEXT_BLOCK(events, blk_5, blk_4, miner_acc); MAKE_NEXT_POS_BLOCK(events, blk_6, blk_5, miner_acc, miner_acc_lst); // 0 11 21 22 23 33 34 35 36 <- height // // +------- blk_1 mined by Alice // | // (0 )- (1 )-...(1r)- (2 )- (3 )-...(3r)- (4 )- (5 )- (6 )- <- main chain // | tx_0 // +---<<---uses-blk_1-out--+ // Case 1 (should pass) // alt PoS block (blk_2a) refers in its coinstake to an output (stake_tx_out_id) already spent in the main chain block blk_2a = AUTO_VAL_INIT(blk_2a); r = generate_pos_block_with_given_coinstake(generator, events, alice_acc, blk_1r, stake_tx, stake_tx_out_id, blk_2a); CHECK_AND_ASSERT_MES(r, false, "generate_pos_block_with_given_coinstake failed"); ADD_CUSTOM_EVENT(events, blk_2a); CHECK_AND_ASSERT_MES(generator.add_block_info(blk_2a, std::list()), false, "add_block_info failed"); // 0 11 21 22 23 33 34 35 36 <- height // // +-----------------------+ // | tx_0 tx_0 spends all outputs in blk_1 (main chain) // (0 )- (1 )-...(1r)- (2 )- (3 )-...(3r)- (4 )- (5 )- (6 )- <- main chain // | \ // | \- (2a)- <- alt chain // +--------------+ PoS block 2a uses stake already spent in main chain // Case 2a (should fail) // alt PoS block (blk_3a) refers in its coinstake to an output (stake_tx_out_id) already spent in this alt chain (in blk_2a) block blk_3a = AUTO_VAL_INIT(blk_3a); r = generate_pos_block_with_given_coinstake(generator, events, alice_acc, blk_2a, stake_tx, stake_tx_out_id, blk_3a); CHECK_AND_ASSERT_MES(r, false, "generate_pos_block_with_given_coinstake failed"); DO_CALLBACK(events, "mark_invalid_block"); ADD_CUSTOM_EVENT(events, blk_3a); // 0 11 21 22 23 33 34 35 36 <- height // // +-----------------------+ // | tx_0 tx_0 spends all outputs in blk_1 (main chain) // (0 )- (1 )-...(1r)- (2 )- (3 )-...(3r)- (4 )- (5 )- (6 )- <- main chain // || \ // || \- (2a)- #3a#- <- alt chain // |+--------------+ | PoS block 2a uses stake already spent in main chain (okay) // +-----------------------+ PoS block 3a uses stake already spent in current alt chain (fail) // Case 2b (should fail) // alt PoS block (blk_3aa) has invalid signature block blk_3aa{}; r = generate_pos_block_with_given_coinstake(generator, events, miner_acc, blk_2a, blk_0r.miner_tx, 0, blk_3aa); CHECK_AND_ASSERT_MES(r, false, "generate_pos_block_with_given_coinstake failed"); if (HF4_active) boost::get(blk_3aa.miner_tx.signatures[0]).y0.m_u64[1] = 5; // invalidate signature else boost::get(blk_3aa.miner_tx.signatures[0]).s[0].c.data[5] = 7; // invalidate signature DO_CALLBACK(events, "mark_invalid_block"); ADD_CUSTOM_EVENT(events, blk_3aa); // 0 11 21 22 23 33 34 35 36 <- height // // +-----------------------+ // | tx_0 tx_0 spends all outputs in blk_1 (main chain) // (0 )- (1 )-...(1r)- (2 )- (3 )-...(3r)- (4 )- (5 )- (6 )- <- main chain // || \ // || \- (2a)- #3aa#- <- alt chain // |+--------------+ | PoS block 2a uses stake already spent in main chain (okay) // +-----------------------+ PoS block 3aa has incorrect signature (fail) REWIND_BLOCKS_N_WITH_TIME(events, blk_2br, blk_2a, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW); // Case 3 (should pass) // alt PoS block (blk_3b) in its coinstake refers to an output from current altchain (from blk_2a) block blk_3b = AUTO_VAL_INIT(blk_3b); r = generate_pos_block_with_given_coinstake(generator, events, alice_acc, blk_2br, blk_2a.miner_tx, 0, blk_3b); CHECK_AND_ASSERT_MES(r, false, "generate_pos_block_with_given_coinstake failed"); ADD_CUSTOM_EVENT(events, blk_3b); CHECK_AND_ASSERT_MES(generator.add_block_info(blk_3b, std::list()), false, "add_block_info failed"); // 0 11 21 22 23 33 34 35 36 <- height // // +-----------------------+ // | tx_0 tx_0 spends all outputs in blk_1 (main chain) // (0 )- (1 )-...(1r)- (2 )- (3 )- ........ (3r)- (4 )- (5 )- (6 )- <- main chain // || \ // || \- (2a)- #3a#- <- alt chain // |+--------------+ \ | PoS block 2a uses stake already spent in main chain (okay) // +---------------|-------+ PoS block 3a uses stake already spent in current alt chain (fail) // | \ // | \ ...... (2br)- (3b)- <- alt chain // | | // +-----------------------+ PoS block 3b uses as stake an output, created in current alt chain (2a) // Case 4 (should fail) // alt PoS block (blk_4b) in its coinstake refers to an output (tx_0) that appeared in the main chain (blk_3) above split height block blk_4b = AUTO_VAL_INIT(blk_4b); r = generate_pos_block_with_given_coinstake(generator, events, alice_acc, blk_3b, tx_0, tx_0_some_output_idx, blk_4b, tx_0_some_output_gindex); if (HF4_active) { CHECK_AND_ASSERT_MES(!r, false, "generate_pos_block_with_given_coinstake not failed as expected"); } else { CHECK_AND_ASSERT_MES(r, false, "generate_pos_block_with_given_coinstake failed"); DO_CALLBACK(events, "mark_invalid_block"); ADD_CUSTOM_EVENT(events, blk_4b); } // 0 11 21 22 23 33 34 35 36 <- height // +------------------+ // +-----------------------+ | | // | tx_0 | tx_0 spends all outputs in blk_1 (main chain) // (0 )- (1 )-...(1r)- (2 )- (3 )- ........ (3r)- (4 )- (5 )- (6 )- <- main chain // || \ | // || \- (2a)- #3a#- | <- alt chain // |+--------------+ \ | \ PoS block 2a uses stake already spent in main chain (okay) // +---------------|-------+ \ PoS block 3a uses stake already spent in current alt chain (fail) // | \ \ // | \ ...... (2br)- (3b)- #4b# <- alt chain // | | // +-----------------------+ PoS block 3b uses as stake an output, created in current alt chain (2a) DO_CALLBACK_PARAMS(events, "check_top_block", params_top_block(get_block_height(blk_6), get_block_hash(blk_6))); // Final check: switch the chains MAKE_NEXT_BLOCK(events, blk_4c, blk_3b, miner_acc); MAKE_NEXT_BLOCK(events, blk_5c, blk_4c, miner_acc); MAKE_NEXT_BLOCK(events, blk_6c, blk_5c, miner_acc); MAKE_NEXT_BLOCK(events, blk_7c, blk_6c, miner_acc); DO_CALLBACK_PARAMS(events, "check_top_block", params_top_block(get_block_height(blk_7c), get_block_hash(blk_7c))); size_t txs_count = 1; DO_CALLBACK_PARAMS(events, "check_tx_pool_count", txs_count); // tx_0 should left in the pool return true; } //------------------------------------------------------------------ pos_minting_tx_packing::pos_minting_tx_packing() : m_pos_mint_packing_size(5) { REGISTER_CALLBACK_METHOD(pos_minting_tx_packing, configure_core); REGISTER_CALLBACK_METHOD(pos_minting_tx_packing, c1); } bool pos_minting_tx_packing::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 = 1; pc.pos_minimum_heigh = 1; c.get_blockchain_storage().set_core_runtime_config(pc); return true; } bool pos_minting_tx_packing::pos_minting_tx_packing::generate(std::vector& events) const { bool r = false; m_accounts.resize(TOTAL_ACCS_COUNT); account_base& miner_acc = m_accounts[MINER_ACC_IDX]; miner_acc.generate(); account_base& alice_acc = m_accounts[ALICE_ACC_IDX]; alice_acc.generate(); std::list miner_acc_lst(1, miner_acc); MAKE_GENESIS_BLOCK(events, blk_0, miner_acc, test_core_time::get_time()); DO_CALLBACK(events, "configure_core"); REWIND_BLOCKS_N_WITH_TIME(events, blk_0r, blk_0, miner_acc, CURRENCY_MINED_MONEY_UNLOCK_WINDOW + 5); m_alice_start_amount = 10 * CURRENCY_BLOCK_REWARD * m_pos_mint_packing_size;// +TESTS_DEFAULT_FEE; // 10 outputs each of (CURRENCY_BLOCK_REWARD * m_pos_mint_packing_size) coins transaction tx_1 = AUTO_VAL_INIT(tx_1); r = construct_tx_with_many_outputs(m_hardforks, events, blk_0r, miner_acc.get_keys(), alice_acc.get_public_address(), m_alice_start_amount, 10, TESTS_DEFAULT_FEE, tx_1); CHECK_AND_ASSERT_MES(r, false, "construct_tx_with_many_outputs failed"); events.push_back(tx_1); MAKE_NEXT_BLOCK_TX1(events, blk_1, blk_0r, miner_acc, tx_1); REWIND_BLOCKS_N_WITH_TIME(events, blk_1r, blk_1, miner_acc, WALLET_DEFAULT_TX_SPENDABLE_AGE); DO_CALLBACK(events, "c1"); return true; } bool pos_minting_tx_packing::c1(currency::core& c, size_t ev_index, const std::vector& events) { bool r = false; std::shared_ptr alice_wlt = init_playtime_test_wallet(events, c, ALICE_ACC_IDX); CHECK_AND_ASSERT_MES(refresh_wallet_and_check_balance("", "Alice", alice_wlt, m_alice_start_amount, true, UINT64_MAX, m_alice_start_amount), false, ""); size_t pos_entries_count = 0; for (size_t i = 0; i < m_pos_mint_packing_size; ++i) { r = mine_next_pos_block_in_playtime_with_wallet(*alice_wlt, m_accounts[ALICE_ACC_IDX].get_public_address(), pos_entries_count); CHECK_AND_ASSERT_MES(r, false, "mine_next_pos_block_in_playtime_with_wallet failed"); alice_wlt->refresh(); } CHECK_AND_ASSERT_MES(refresh_wallet_and_check_balance("", "Alice", alice_wlt, m_alice_start_amount + CURRENCY_BLOCK_REWARD * m_pos_mint_packing_size, true, UINT64_MAX), false, ""); r = mine_next_pow_blocks_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c, WALLET_DEFAULT_TX_SPENDABLE_AGE); CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_blocks_in_playtime failed"); CHECK_AND_ASSERT_MES(refresh_wallet_and_check_balance("", "Alice", alice_wlt, m_alice_start_amount + CURRENCY_BLOCK_REWARD * m_pos_mint_packing_size, // total true, UINT64_MAX, m_alice_start_amount + CURRENCY_BLOCK_REWARD * m_pos_mint_packing_size // unlocked ), false, ""); alice_wlt->set_defragmentation_tx_settings(true, m_pos_mint_packing_size + 1, m_pos_mint_packing_size + 1); // +1 because previous implementation () had an error with this limit // no coinbase tx outputs should be packed r = alice_wlt->try_mint_pos(); CHECK_AND_ASSERT_MES(r, false, "try_mint_pos failed"); // make sure the wallet has only received new locked incoming reward CHECK_AND_ASSERT_MES(refresh_wallet_and_check_balance("", "Alice", alice_wlt, m_alice_start_amount + CURRENCY_BLOCK_REWARD * (m_pos_mint_packing_size + 1), // total true, UINT64_MAX, m_alice_start_amount // unlocked (one output with amount == CURRENCY_BLOCK_REWARD * m_pos_mint_packing_size was spent as stake) ), false, ""); r = mine_next_pow_blocks_in_playtime(m_accounts[MINER_ACC_IDX].get_public_address(), c, WALLET_DEFAULT_TX_SPENDABLE_AGE); CHECK_AND_ASSERT_MES(r, false, "mine_next_pow_blocks_in_playtime failed"); CHECK_AND_ASSERT_MES(refresh_wallet_and_check_balance("", "Alice", alice_wlt, m_alice_start_amount + CURRENCY_BLOCK_REWARD * (m_pos_mint_packing_size + 1), // total true, UINT64_MAX, m_alice_start_amount + CURRENCY_BLOCK_REWARD * (m_pos_mint_packing_size + 1) // unlocked ), false, ""); // coinbase tx outputs should be packed now, there's enough coinbase outputs (> m_pos_mint_packing_size) r = alice_wlt->try_mint_pos(); CHECK_AND_ASSERT_MES(r, false, "try_mint_pos failed"); CHECK_AND_ASSERT_MES(refresh_wallet_and_check_balance("", "Alice", alice_wlt, m_alice_start_amount + CURRENCY_BLOCK_REWARD * (m_pos_mint_packing_size + 2), // total (+1 one block reward) true, UINT64_MAX, // CURRENCY_BLOCK_REWARD * m_pos_mint_packing_size locked for stake // CURRENCY_BLOCK_REWARD * (m_pos_mint_packing_size + 1) locked for packing tx m_alice_start_amount + CURRENCY_BLOCK_REWARD - CURRENCY_BLOCK_REWARD * (m_pos_mint_packing_size + 1) // unlocked ), false, ""); r = alice_wlt->try_mint_pos(); CHECK_AND_ASSERT_MES(r, false, "try_mint_pos failed"); CHECK_AND_ASSERT_MES(refresh_wallet_and_check_balance("", "Alice", alice_wlt, m_alice_start_amount + CURRENCY_BLOCK_REWARD * (m_pos_mint_packing_size + 3), // total (+1 one block reward) true, UINT64_MAX, // CURRENCY_BLOCK_REWARD * m_pos_mint_packing_size locked for stake m_alice_start_amount + CURRENCY_BLOCK_REWARD - CURRENCY_BLOCK_REWARD * (m_pos_mint_packing_size + 1) - CURRENCY_BLOCK_REWARD * m_pos_mint_packing_size // unlocked ), false, ""); return true; }