diff --git a/src/blockchain_db/sqlite/db_sqlite.cpp b/src/blockchain_db/sqlite/db_sqlite.cpp index 2eece7795..4bc9b2321 100644 --- a/src/blockchain_db/sqlite/db_sqlite.cpp +++ b/src/blockchain_db/sqlite/db_sqlite.cpp @@ -233,7 +233,7 @@ namespace cryptonote { // This calculates the operator fee using (operator_portion / max_operator_portion) * distribution_amount but using 128 bit integer math uint64_t hi, lo, resulthi, resultlo; lo = mul128(sn_info.portions_for_operator, distribution_amount, &hi); - div128_64(hi, lo, STAKING_PORTIONS, &resulthi, &resultlo); + div128_64(hi, lo, STAKING_PORTIONS_V1, &resulthi, &resultlo); if (resulthi > 0) throw std::logic_error("overflow from calculating sn operator fee"); operator_fee = resultlo; diff --git a/src/cryptonote_basic/cryptonote_format_utils.cpp b/src/cryptonote_basic/cryptonote_format_utils.cpp index d4f3f57fe..a13e83859 100644 --- a/src/cryptonote_basic/cryptonote_format_utils.cpp +++ b/src/cryptonote_basic/cryptonote_format_utils.cpp @@ -1321,12 +1321,12 @@ namespace cryptonote LOG_ERROR("get_registration_hash addresses.size() != portions.size()"); return false; } - uint64_t portions_left = STAKING_PORTIONS; + uint64_t portions_left = STAKING_PORTIONS_V1; for (uint64_t portion : portions) { if (portion > portions_left) { - LOG_ERROR(tr("Your registration has more than ") << STAKING_PORTIONS << tr(" portions, this registration is invalid!")); + LOG_ERROR(tr("Your registration has more than ") << STAKING_PORTIONS_V1 << tr(" portions, this registration is invalid!")); return false; } portions_left -= portion; diff --git a/src/cryptonote_config.h b/src/cryptonote_config.h index eb83d0a1c..d7d5f983b 100644 --- a/src/cryptonote_config.h +++ b/src/cryptonote_config.h @@ -51,11 +51,12 @@ using namespace std::literals; #define CRYPTONOTE_DEFAULT_TX_MIXIN 9 #define STAKING_REQUIREMENT_LOCK_BLOCKS_EXCESS 20 -#define STAKING_PORTIONS UINT64_C(0xfffffffffffffffc) -#define MAX_NUMBER_OF_CONTRIBUTORS 4 -#define MIN_PORTIONS (STAKING_PORTIONS / MAX_NUMBER_OF_CONTRIBUTORS) +#define STAKING_PORTIONS_V1 UINT64_C(0xfffffffffffffffc) +#define MAX_NUMBER_OF_CONTRIBUTORS_V1 4 +#define MAX_NUMBER_OF_CONTRIBUTORS_V2 10 +#define MIN_PORTIONS_V2 (STAKING_PORTIONS_V1 / MAX_NUMBER_OF_CONTRIBUTORS_V2) -static_assert(STAKING_PORTIONS % 12 == 0, "Use a multiple of twelve, so that it divides evenly by two, three, or four contributors."); +static_assert(STAKING_PORTIONS_V1 % 12 == 0, "Use a multiple of 12, so that it divides evenly by two, three, or four contributors."); #define STAKING_AUTHORIZATION_EXPIRATION_WINDOW (60*60*24*7*2) // 2 weeks diff --git a/src/cryptonote_core/cryptonote_tx_utils.cpp b/src/cryptonote_core/cryptonote_tx_utils.cpp index 636fd28b4..ffe504d54 100644 --- a/src/cryptonote_core/cryptonote_tx_utils.cpp +++ b/src/cryptonote_core/cryptonote_tx_utils.cpp @@ -210,7 +210,7 @@ namespace cryptonote { uint64_t hi, lo, rewardhi, rewardlo; lo = mul128(total_service_node_reward, portions, &hi); - div128_64(hi, lo, STAKING_PORTIONS, &rewardhi, &rewardlo); + div128_64(hi, lo, STAKING_PORTIONS_V1, &rewardhi, &rewardlo); return rewardlo; } diff --git a/src/cryptonote_core/service_node_list.cpp b/src/cryptonote_core/service_node_list.cpp index 3988f9494..4e477922a 100644 --- a/src/cryptonote_core/service_node_list.cpp +++ b/src/cryptonote_core/service_node_list.cpp @@ -333,13 +333,14 @@ namespace service_nodes void validate_contributor_args(uint8_t hf_version, contributor_args_t const &contributor_args) { + const size_t max_contributors = hf_version >= 19 ? MAX_NUMBER_OF_CONTRIBUTORS_V2 : MAX_NUMBER_OF_CONTRIBUTORS_V1; if (contributor_args.portions.empty()) throw invalid_contributions{"No portions given"}; if (contributor_args.portions.size() != contributor_args.addresses.size()) throw invalid_contributions{"Number of portions (" + std::to_string(contributor_args.portions.size()) + ") doesn't match the number of addresses (" + std::to_string(contributor_args.portions.size()) + ")"}; - if (contributor_args.portions.size() > MAX_NUMBER_OF_CONTRIBUTORS) + if (contributor_args.portions.size() > max_contributors) throw invalid_contributions{"Too many contributors"}; - if (contributor_args.portions_for_operator > STAKING_PORTIONS) + if (contributor_args.portions_for_operator > STAKING_PORTIONS_V1) throw invalid_contributions{"Operator portions are too high"}; if (!check_service_node_portions(hf_version, contributor_args.portions)) @@ -940,10 +941,10 @@ namespace service_nodes // Don't need this check for HF16+ because the number of reserved spots is already checked in // the registration details, and we disallow a non-operator registration. - if (total_num_of_addr > MAX_NUMBER_OF_CONTRIBUTORS) + if (total_num_of_addr > MAX_NUMBER_OF_CONTRIBUTORS_V1) { LOG_PRINT_L1("Register TX: Number of participants: " << total_num_of_addr << - " exceeded the max number of contributors: " << MAX_NUMBER_OF_CONTRIBUTORS << + " exceeded the max number of contributors: " << MAX_NUMBER_OF_CONTRIBUTORS_V1 << " on height: " << block_height << " for tx: " << cryptonote::get_transaction_hash(tx)); return false; @@ -976,7 +977,7 @@ namespace service_nodes uint64_t hi, lo, resulthi, resultlo; lo = mul128(info.staking_requirement, contributor_args.portions[i], &hi); - div128_64(hi, lo, STAKING_PORTIONS, &resulthi, &resultlo); + div128_64(hi, lo, STAKING_PORTIONS_V1, &resulthi, &resultlo); info.contributors.emplace_back(); auto &contributor = info.contributors.back(); @@ -1145,20 +1146,23 @@ namespace service_nodes // Check node contributor counts { bool too_many_contributions = false; - if (hf_version >= cryptonote::network_version_16_pulse) + if (hf_version >= cryptonote::network_version_19) + // As of HF19 we allow up to 10 stakes total + too_many_contributions = existing_contributions + stake.locked_contributions.size() > MAX_NUMBER_OF_CONTRIBUTORS_V2; + else if (hf_version >= cryptonote::network_version_16_pulse) // Before HF16 we didn't properly take into account unfilled reservation spots - too_many_contributions = existing_contributions + other_reservations + 1 > MAX_NUMBER_OF_CONTRIBUTORS; + too_many_contributions = existing_contributions + other_reservations + 1 > MAX_NUMBER_OF_CONTRIBUTORS_V1; else if (hf_version >= cryptonote::network_version_11_infinite_staking) // As of HF11 we allow up to 4 stakes total (except for the loophole closed above) - too_many_contributions = existing_contributions + stake.locked_contributions.size() > MAX_NUMBER_OF_CONTRIBUTORS; + too_many_contributions = existing_contributions + stake.locked_contributions.size() > MAX_NUMBER_OF_CONTRIBUTORS_V1; else // Before HF11 we allowed up to 4 contributors, but each can contribute multiple times - too_many_contributions = new_contributor && contributors.size() >= MAX_NUMBER_OF_CONTRIBUTORS; + too_many_contributions = new_contributor && contributors.size() >= MAX_NUMBER_OF_CONTRIBUTORS_V1; if (too_many_contributions) { LOG_PRINT_L1("TX: Already hit the max number of contributions: " - << MAX_NUMBER_OF_CONTRIBUTORS + << (hf_version >= 19 ? MAX_NUMBER_OF_CONTRIBUTORS_V2 : MAX_NUMBER_OF_CONTRIBUTORS_V1) << " for contributor: " << cryptonote::get_account_address_as_str(nettype, false, stake.address) << " on height: " << block_height << " for tx: " << cryptonote::get_transaction_hash(tx)); return false; @@ -3665,24 +3669,25 @@ namespace service_nodes return result; } - if ((args.size()-1)/ 2 > MAX_NUMBER_OF_CONTRIBUTORS) + const size_t max_contributors = hf_version >= 19 ? MAX_NUMBER_OF_CONTRIBUTORS_V2 : MAX_NUMBER_OF_CONTRIBUTORS_V1; + if (args.size() > 1 + 2 * max_contributors) { - result.err_msg = tr("Exceeds the maximum number of contributors, which is ") + std::to_string(MAX_NUMBER_OF_CONTRIBUTORS); + result.err_msg = tr("Exceeds the maximum number of contributors, which is ") + std::to_string(max_contributors); return result; } try { result.portions_for_operator = boost::lexical_cast(args[0]); - if (result.portions_for_operator > STAKING_PORTIONS) + if (result.portions_for_operator > STAKING_PORTIONS_V1) { - result.err_msg = tr("Invalid portion amount: ") + args[0] + tr(". Must be between 0 and ") + std::to_string(STAKING_PORTIONS); + result.err_msg = tr("Invalid portion amount: ") + args[0] + tr(". Must be between 0 and ") + std::to_string(STAKING_PORTIONS_V1); return result; } } catch (const std::exception &e) { - result.err_msg = tr("Invalid portion amount: ") + args[0] + tr(". Must be between 0 and ") + std::to_string(STAKING_PORTIONS); + result.err_msg = tr("Invalid portion amount: ") + args[0] + tr(". Must be between 0 and ") + std::to_string(STAKING_PORTIONS_V1); return result; } @@ -3729,13 +3734,14 @@ namespace service_nodes } } + // // FIXME(doyle): FIXME(oxen) !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // This is temporary code to redistribute the insufficient portion dust // amounts between contributors. It should be removed in HF12. // - std::array excess_portions; - std::array min_contributions; + std::array excess_portions; + std::array min_contributions; { // NOTE: Calculate excess portions from each contributor uint64_t oxen_reserved = 0; @@ -3755,7 +3761,7 @@ namespace service_nodes } } - uint64_t portions_left = STAKING_PORTIONS; + uint64_t portions_left = STAKING_PORTIONS_V1; uint64_t total_reserved = 0; for (size_t i = 0; i < addr_to_portions.size(); ++i) { @@ -3769,7 +3775,7 @@ namespace service_nodes // the minimum by a dust amount. uint64_t needed = min_portions - addr_to_portion.portions; const uint64_t FUDGE_FACTOR = 10; - const uint64_t DUST_UNIT = (STAKING_PORTIONS / staking_requirement); + const uint64_t DUST_UNIT = (STAKING_PORTIONS_V1 / staking_requirement); const uint64_t DUST = DUST_UNIT * FUDGE_FACTOR; if (needed > DUST) continue; @@ -3793,7 +3799,7 @@ namespace service_nodes // NOTE: Operator is sending in the minimum amount and it falls below // the minimum by dust, just increase the portions so it passes - if (needed > 0 && addr_to_portions.size() < MAX_NUMBER_OF_CONTRIBUTORS) + if (needed > 0 && addr_to_portions.size() < MAX_NUMBER_OF_CONTRIBUTORS_V2) addr_to_portion.portions += needed; } @@ -3805,7 +3811,7 @@ namespace service_nodes if (min_portions == UINT64_MAX) { - result.err_msg = tr("Too many contributors specified, you can only split a node with up to: ") + std::to_string(MAX_NUMBER_OF_CONTRIBUTORS) + tr(" people."); + result.err_msg = tr("Too many contributors specified, you can only split a node with up to: ") + std::to_string(max_contributors) + tr(" people."); return result; } @@ -3817,6 +3823,7 @@ namespace service_nodes total_reserved += oxen_amount; } + result.success = true; return result; } @@ -3840,6 +3847,7 @@ namespace service_nodes uint64_t exp_timestamp = time(nullptr) + STAKING_AUTHORIZATION_EXPIRATION_WINDOW; crypto::hash hash; + bool hashed = cryptonote::get_registration_hash(contributor_args.addresses, contributor_args.portions_for_operator, contributor_args.portions, exp_timestamp, hash); if (!hashed) { @@ -3959,7 +3967,7 @@ namespace service_nodes // Add contributors and their portions to winners. result.payouts.reserve(info.contributors.size()); - const uint64_t remaining_portions = STAKING_PORTIONS - info.portions_for_operator; + const uint64_t remaining_portions = STAKING_PORTIONS_V1 - info.portions_for_operator; for (const auto& contributor : info.contributors) { uint64_t hi, lo, resulthi, resultlo; diff --git a/src/cryptonote_core/service_node_list.h b/src/cryptonote_core/service_node_list.h index 66aebac8c..0a1fcabb1 100644 --- a/src/cryptonote_core/service_node_list.h +++ b/src/cryptonote_core/service_node_list.h @@ -837,6 +837,6 @@ namespace service_nodes payout service_node_info_to_payout(crypto::public_key const &key, service_node_info const &info); - const static payout_entry null_payout_entry = {cryptonote::null_address, STAKING_PORTIONS}; + const static payout_entry null_payout_entry = {cryptonote::null_address, STAKING_PORTIONS_V1}; const static payout null_payout = {crypto::null_pkey, {null_payout_entry}}; } diff --git a/src/cryptonote_core/service_node_rules.cpp b/src/cryptonote_core/service_node_rules.cpp index 9df7bceae..83040ecff 100644 --- a/src/cryptonote_core/service_node_rules.cpp +++ b/src/cryptonote_core/service_node_rules.cpp @@ -89,23 +89,32 @@ uint64_t portions_to_amount(uint64_t portions, uint64_t staking_requirement) { uint64_t hi, lo, resulthi, resultlo; lo = mul128(staking_requirement, portions, &hi); - div128_64(hi, lo, STAKING_PORTIONS, &resulthi, &resultlo); + div128_64(hi, lo, STAKING_PORTIONS_V1, &resulthi, &resultlo); return resultlo; } bool check_service_node_portions(uint8_t hf_version, const std::vector& portions) { - if (portions.size() > MAX_NUMBER_OF_CONTRIBUTORS) return false; + + const size_t max_contributors = hf_version >= 19 ? MAX_NUMBER_OF_CONTRIBUTORS_V2 : MAX_NUMBER_OF_CONTRIBUTORS_V1; + if (portions.size() > max_contributors) return false; + + if (portions[0] < get_min_node_operator_contribution(STAKING_PORTIONS_V1)) + { + LOG_PRINT_L1("Register TX rejected: TX does not have sufficient operator stake"); + return false; + } uint64_t reserved = 0; for (auto i = 0u; i < portions.size(); ++i) { - const uint64_t min_portions = get_min_node_contribution(hf_version, STAKING_PORTIONS, reserved, i); + + const uint64_t min_portions = get_min_node_contribution(hf_version, STAKING_PORTIONS_V1, reserved, i); if (portions[i] < min_portions) return false; reserved += portions[i]; } - return reserved <= STAKING_PORTIONS; + return reserved <= STAKING_PORTIONS_V1; } crypto::hash generate_request_stake_unlock_hash(uint32_t nonce) @@ -127,7 +136,7 @@ uint64_t get_locked_key_image_unlock_height(cryptonote::network_type nettype, ui static uint64_t get_min_node_contribution_pre_v11(uint64_t staking_requirement, uint64_t total_reserved) { - return std::min(staking_requirement - total_reserved, staking_requirement / MAX_NUMBER_OF_CONTRIBUTORS); + return std::min(staking_requirement - total_reserved, staking_requirement / MAX_NUMBER_OF_CONTRIBUTORS_V1); } uint64_t get_max_node_contribution(uint8_t version, uint64_t staking_requirement, uint64_t total_reserved) @@ -144,10 +153,12 @@ uint64_t get_min_node_contribution(uint8_t version, uint64_t staking_requirement return get_min_node_contribution_pre_v11(staking_requirement, total_reserved); const uint64_t needed = staking_requirement - total_reserved; - assert(MAX_NUMBER_OF_CONTRIBUTORS > num_contributions); - if (MAX_NUMBER_OF_CONTRIBUTORS <= num_contributions) return UINT64_MAX; - const size_t num_contributions_remaining_avail = MAX_NUMBER_OF_CONTRIBUTORS - num_contributions; + const size_t max_contributors = (version < cryptonote::network_version_19) ? MAX_NUMBER_OF_CONTRIBUTORS_V1: MAX_NUMBER_OF_CONTRIBUTORS_V2; + assert(max_contributors > num_contributions); + if (max_contributors <= num_contributions) return UINT64_MAX; + + const size_t num_contributions_remaining_avail = max_contributors - num_contributions; return needed / num_contributions_remaining_avail; } @@ -158,6 +169,11 @@ uint64_t get_min_node_contribution_in_portions(uint8_t version, uint64_t staking return result; } +uint64_t get_min_node_operator_contribution(uint64_t staking_requirement) +{ + return staking_requirement / MAX_NUMBER_OF_CONTRIBUTORS_V1; +} + uint64_t get_portions_to_make_amount(uint64_t staking_requirement, uint64_t amount, uint64_t max_portions) { uint64_t lo, hi, resulthi, resultlo; @@ -175,11 +191,11 @@ static bool get_portions_from_percent(double cur_percent, uint64_t& portions) { // Fix for truncation issue when operator cut = 100 for a pool Service Node. if (cur_percent == 100.0) { - portions = STAKING_PORTIONS; + portions = STAKING_PORTIONS_V1; } else { - portions = (cur_percent / 100.0) * (double)STAKING_PORTIONS; + portions = (cur_percent / 100.0) * (double)STAKING_PORTIONS_V1; } return true; diff --git a/src/cryptonote_core/service_node_rules.h b/src/cryptonote_core/service_node_rules.h index 631fcaf2f..20367dfe7 100644 --- a/src/cryptonote_core/service_node_rules.h +++ b/src/cryptonote_core/service_node_rules.h @@ -241,10 +241,11 @@ namespace service_nodes { constexpr uint8_t MAXIMUM_EXTERNAL_OUT_OF_SYNC = 80; -static_assert(STAKING_PORTIONS != UINT64_MAX, "UINT64_MAX is used as the invalid value for failing to calculate the min_node_contribution"); +static_assert(STAKING_PORTIONS_V1 != UINT64_MAX, "UINT64_MAX is used as the invalid value for failing to calculate the min_node_contribution"); // return: UINT64_MAX if (num_contributions > the max number of contributions), otherwise the amount in oxen atomic units uint64_t get_min_node_contribution (uint8_t version, uint64_t staking_requirement, uint64_t total_reserved, size_t num_contributions); uint64_t get_min_node_contribution_in_portions(uint8_t version, uint64_t staking_requirement, uint64_t total_reserved, size_t num_contributions); +uint64_t get_min_node_operator_contribution(uint64_t staking_requirement); // Gets the maximum allowed stake amount. This is used to prevent significant overstaking. The // wallet tries to avoid this when submitting a stake, but it can still happen when competing stakes @@ -267,7 +268,7 @@ crypto::hash generate_request_stake_unlock_hash(uint32_t nonce); uint64_t get_locked_key_image_unlock_height(cryptonote::network_type nettype, uint64_t node_register_height, uint64_t curr_height); // Returns lowest x such that (staking_requirement * x/STAKING_PORTIONS) >= amount -uint64_t get_portions_to_make_amount(uint64_t staking_requirement, uint64_t amount, uint64_t max_portions = STAKING_PORTIONS); +uint64_t get_portions_to_make_amount(uint64_t staking_requirement, uint64_t amount, uint64_t max_portions = STAKING_PORTIONS_V1); bool get_portions_from_percent_str(std::string cut_str, uint64_t& portions); diff --git a/src/daemon/rpc_command_executor.cpp b/src/daemon/rpc_command_executor.cpp index 0f7c2ba63..7d34c6aa7 100644 --- a/src/daemon/rpc_command_executor.cpp +++ b/src/daemon/rpc_command_executor.cpp @@ -1582,7 +1582,7 @@ static void append_printable_service_node_list_entry(cryptonote::network_type ne if (detailed_view) // Print operator information { - stream << indent2 << "Operator Cut (\% Of Reward): " << to_string_rounded((entry.portions_for_operator / (double)STAKING_PORTIONS) * 100.0, 2) << "%\n"; + stream << indent2 << "Operator Cut (\% Of Reward): " << to_string_rounded((entry.portions_for_operator / (double)STAKING_PORTIONS_V1) * 100.0, 2) << "%\n"; stream << indent2 << "Operator Address: " << entry.operator_address << "\n"; } @@ -1891,7 +1891,7 @@ static uint64_t get_actual_amount(uint64_t amount, uint64_t portions) { uint64_t lo, hi, resulthi, resultlo; lo = mul128(amount, portions, &hi); - div128_64(hi, lo, STAKING_PORTIONS, &resulthi, &resultlo); + div128_64(hi, lo, STAKING_PORTIONS_V1, &resulthi, &resultlo); return resultlo; } @@ -2003,8 +2003,8 @@ bool rpc_command_executor::prepare_registration(bool force_registration) }; // anything less than DUST will be added to operator stake - const uint64_t DUST = MAX_NUMBER_OF_CONTRIBUTORS; - std::cout << "Current staking requirement: " << highlight_money(staking_requirement) << std::endl; + const uint64_t DUST = MAX_NUMBER_OF_CONTRIBUTORS_V2; + std::cout << "Current staking requirement: " << cryptonote::print_money(staking_requirement) << " " << cryptonote::get_unit() << std::endl; enum struct register_step { @@ -2025,8 +2025,8 @@ bool rpc_command_executor::prepare_registration(bool force_registration) { register_step prev_step = register_step::ask_address; size_t num_participants = 1; - uint64_t operator_fee_portions = STAKING_PORTIONS; - uint64_t portions_remaining = STAKING_PORTIONS; + uint64_t operator_fee_portions = STAKING_PORTIONS_V1; + uint64_t portions_remaining = STAKING_PORTIONS_V1; uint64_t total_reserved_contributions = 0; std::vector addresses; std::vector contributions; @@ -2114,7 +2114,7 @@ bool rpc_command_executor::prepare_registration(bool force_registration) if (result == input_line_result::yes) { std::cout << std::endl; - state.contributions.push_back(STAKING_PORTIONS); + state.contributions.push_back(STAKING_PORTIONS_V1); state.portions_remaining = 0; state.total_reserved_contributions += staking_requirement; @@ -2158,16 +2158,16 @@ bool rpc_command_executor::prepare_registration(bool force_registration) case register_step::how_many_more_contributors: { auto [result, input] = input_line_value( - "Number of additional contributors [1-" + std::to_string(MAX_NUMBER_OF_CONTRIBUTORS - 1) + "]"); + "Number of additional contributors [1-" + std::to_string(MAX_NUMBER_OF_CONTRIBUTORS_V2 - 1) + "]"); if (check_cancel_back(result)) break; size_t additional_contributors; if (!tools::parse_int(input, additional_contributors) || - additional_contributors < 1 || additional_contributors > (MAX_NUMBER_OF_CONTRIBUTORS - 1)) + additional_contributors < 1 || additional_contributors > (MAX_NUMBER_OF_CONTRIBUTORS_V2 - 1)) { - tools::fail_msg_writer() << "Invalid value; must be between 1 and " << (MAX_NUMBER_OF_CONTRIBUTORS - 1) << "." << std::endl; + tools::fail_msg_writer() << "Invalid value; must be between 1 and " << (MAX_NUMBER_OF_CONTRIBUTORS_V2 - 1) << "." << std::endl; break; } @@ -2182,7 +2182,7 @@ bool rpc_command_executor::prepare_registration(bool force_registration) uint64_t amount_left = staking_requirement - state.total_reserved_contributions; uint64_t min_contribution_portions = service_nodes::get_min_node_contribution_in_portions( hf_version, staking_requirement, state.total_reserved_contributions, state.contributions.size()); - uint64_t min_contribution = service_nodes::portions_to_amount(staking_requirement, min_contribution_portions); + uint64_t min_contribution = is_operator ? service_nodes::get_min_node_operator_contribution(staking_requirement) : service_nodes::portions_to_amount(staking_requirement, min_contribution_portions); auto [result, contribution_str] = input_line_value(fmt::format( "The {} contribution must be between {} and {} to meet the staking requirements.\n\n" @@ -2236,7 +2236,7 @@ bool rpc_command_executor::prepare_registration(bool force_registration) case register_step::summary_info: { - uint64_t open_spots = MAX_NUMBER_OF_CONTRIBUTORS - state.contributions.size(); + uint64_t open_spots = MAX_NUMBER_OF_CONTRIBUTORS_V2 - state.contributions.size(); const uint64_t amount_left = staking_requirement - state.total_reserved_contributions; fmt::print("Total reserved contributions: {}\n", highlight_money(state.total_reserved_contributions)); if (amount_left > DUST) @@ -2276,7 +2276,7 @@ bool rpc_command_executor::prepare_registration(bool force_registration) if (amount_left > 0 || state.addresses.size() > 1) fmt::print("Operator fee (as % of Service Node rewards): \x1b[33;1m{}%\x1b[0m\n\n", - state.operator_fee_portions * 100.0 / static_cast(STAKING_PORTIONS)); + state.operator_fee_portions * 100.0 / static_cast(STAKING_PORTIONS_V1)); constexpr auto row = "{:^14} {:^13} {:>17} {:>8}\n"sv; fmt::print(row, "Contributor", "Address", "Contribution", "Contr. %"); @@ -2293,12 +2293,12 @@ bool rpc_command_executor::prepare_registration(bool force_registration) (i==0) ? "Operator" : "Contributor " + std::to_string(i), addr.substr(0, 9) + ".." + addr.substr(addr.size() - 2), cryptonote::print_money(amount), - fmt::format("{:.2f}%", state.contributions[i] * 100.0 / STAKING_PORTIONS)); + fmt::format("{:.2f}%", state.contributions[i] * 100.0 / STAKING_PORTIONS_V1)); } if (amount_left > DUST) { - size_t open_spots = MAX_NUMBER_OF_CONTRIBUTORS - state.contributions.size(); + size_t open_spots = MAX_NUMBER_OF_CONTRIBUTORS_V2 - state.contributions.size(); for (size_t i = 0; i < open_spots; i++) { fmt::print(row, "(open)", diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 504d7b5e8..75dfe7de7 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -8137,6 +8137,7 @@ wallet2::stake_result wallet2::check_stake_allowed(const crypto::public_key& sn_ uint8_t const hf_version = *res; uint64_t max_contrib_total = snode_info.staking_requirement - snode_info.total_reserved; + uint64_t min_contrib_total = service_nodes::get_min_node_contribution(hf_version, snode_info.staking_requirement, snode_info.total_reserved, total_existing_contributions); bool is_preexisting_contributor = false; @@ -8165,7 +8166,7 @@ wallet2::stake_result wallet2::check_stake_allowed(const crypto::public_key& sn_ return result; } - const bool full = snode_info.contributors.size() >= MAX_NUMBER_OF_CONTRIBUTORS; + const bool full = snode_info.contributors.size() >= MAX_NUMBER_OF_CONTRIBUTORS_V2; if (full && !is_preexisting_contributor) { result.status = stake_result_status::service_node_contributors_maxed; @@ -8175,7 +8176,7 @@ wallet2::stake_result wallet2::check_stake_allowed(const crypto::public_key& sn_ if (amount < min_contrib_total) { - const uint64_t DUST = MAX_NUMBER_OF_CONTRIBUTORS; + const uint64_t DUST = MAX_NUMBER_OF_CONTRIBUTORS_V2; if (min_contrib_total - amount <= DUST) { amount = min_contrib_total; @@ -8225,7 +8226,9 @@ wallet2::stake_result wallet2::create_stake_tx(const crypto::public_key& service { result = check_stake_allowed(service_node_key, addr_info, amount, amount_fraction); if (result.status != stake_result_status::success) + { return result; + } } catch (const std::exception& e) { @@ -8498,7 +8501,7 @@ wallet2::register_service_node_result wallet2::create_register_service_node_tx(c { uint64_t amount_payable_by_operator = 0; { - const uint64_t DUST = MAX_NUMBER_OF_CONTRIBUTORS; + const uint64_t DUST = MAX_NUMBER_OF_CONTRIBUTORS_V2; uint64_t amount_left = staking_requirement; for (size_t i = 0; i < contributor_args.portions.size(); i++) { diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 2d10634eb..32b3ea8c2 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -2981,6 +2981,7 @@ namespace { // NOTE(oxen): Pre-emptively set subaddr_account to 0. We don't support onwards from Infinite Staking which is when this call was implemented. tools::wallet2::register_service_node_result register_result = m_wallet->create_register_service_node_tx(args, 0 /*subaddr_account*/); + if (register_result.status != tools::wallet2::register_service_node_result_status::success) throw wallet_rpc_error{error_code::TX_NOT_POSSIBLE, register_result.msg}; diff --git a/tests/core_tests/chaingen.h b/tests/core_tests/chaingen.h index e5b296a65..72d0d1a02 100644 --- a/tests/core_tests/chaingen.h +++ b/tests/core_tests/chaingen.h @@ -1493,7 +1493,7 @@ struct oxen_chain_generator cryptonote::transaction create_tx(const cryptonote::account_base &src, const cryptonote::account_public_address &dest, uint64_t amount, uint64_t fee) const; cryptonote::transaction create_registration_tx(const cryptonote::account_base &src, const cryptonote::keypair &service_node_keys = cryptonote::keypair{hw::get_device("default")}, - uint64_t src_portions = STAKING_PORTIONS, + uint64_t src_portions = STAKING_PORTIONS_V1, uint64_t src_operator_cut = 0, std::array const &contributors = {}, int num_contributors = 0) const; diff --git a/tests/core_tests/chaingen_main.cpp b/tests/core_tests/chaingen_main.cpp index 0957c4c32..7f5a05265 100644 --- a/tests/core_tests/chaingen_main.cpp +++ b/tests/core_tests/chaingen_main.cpp @@ -134,6 +134,9 @@ int main(int argc, char* argv[]) GENERATE_AND_PLAY(oxen_service_nodes_checkpoint_quorum_size); GENERATE_AND_PLAY(oxen_service_nodes_gen_nodes); GENERATE_AND_PLAY(oxen_service_nodes_insufficient_contribution); + GENERATE_AND_PLAY(oxen_service_nodes_insufficient_contribution_HF18); + GENERATE_AND_PLAY(oxen_service_nodes_sufficient_contribution_HF19); + GENERATE_AND_PLAY(oxen_service_nodes_insufficient_operator_contribution_HF19); GENERATE_AND_PLAY(oxen_service_nodes_test_rollback); GENERATE_AND_PLAY(oxen_service_nodes_test_swarms_basic); GENERATE_AND_PLAY(oxen_pulse_invalid_validator_bitset); diff --git a/tests/core_tests/oxen_tests.cpp b/tests/core_tests/oxen_tests.cpp index 15810194d..849751cbf 100644 --- a/tests/core_tests/oxen_tests.cpp +++ b/tests/core_tests/oxen_tests.cpp @@ -2980,14 +2980,20 @@ bool oxen_service_nodes_insufficient_contribution::generate(std::vector &events) +{ + auto hard_forks = oxen_generate_hard_fork_table(cryptonote::network_version_19 - 1); + oxen_chain_generator gen(events, hard_forks); + + gen.add_blocks_until_version(hard_forks.back().version); + gen.add_mined_money_unlock_blocks(); + + const auto alice = gen.add_account(); + const auto tx0 = gen.create_and_add_tx(gen.first_miner_, alice.get_keys().m_account_address, MK_COINS(101)); + gen.create_and_add_next_block({tx0}); + gen.add_transfer_unlock_blocks(); + + uint64_t operator_portions = STAKING_PORTIONS_V1 / MAX_NUMBER_OF_CONTRIBUTORS_V1; + uint64_t operator_amount = service_nodes::get_staking_requirement(cryptonote::FAKECHAIN, hard_forks.back().height) / MAX_NUMBER_OF_CONTRIBUTORS_V1; + uint64_t remaining_portions = STAKING_PORTIONS_V1 - operator_portions; + uint64_t single_portion_illegal_HF18_legal_HF19 = remaining_portions / (MAX_NUMBER_OF_CONTRIBUTORS_V2 - 1); + uint64_t single_contributed_amount = (service_nodes::get_staking_requirement(cryptonote::FAKECHAIN, hard_forks.back().height) - operator_amount) / (MAX_NUMBER_OF_CONTRIBUTORS_V2 - 1); + cryptonote::keypair sn_keys{hw::get_device("default")}; + cryptonote::transaction register_tx = gen.create_registration_tx(gen.first_miner_, sn_keys, operator_portions); + gen.add_tx(register_tx); + gen.create_and_add_next_block({register_tx}); + gen.add_transfer_unlock_blocks(); + + assert(single_contributed_amount != 0); + cryptonote::transaction stake = gen.create_and_add_staking_tx(sn_keys.pub, alice, single_contributed_amount); + gen.create_and_add_next_block({stake}); + + oxen_register_callback(events, "test_insufficient_stake_does_not_get_accepted", [sn_keys, operator_amount](cryptonote::core &c, size_t ev_index) + { + DEFINE_TESTS_ERROR_CONTEXT("test_insufficient_stake_does_not_get_accepted"); + const auto sn_list = c.get_service_node_list_state({sn_keys.pub}); + CHECK_TEST_CONDITION(sn_list.size() == 1); + CHECK_TEST_CONDITION(sn_list[0].info->contributors.size() == 1); + + service_nodes::service_node_pubkey_info const &pubkey_info = sn_list[0]; + CHECK_EQ(pubkey_info.info->total_contributed, operator_amount); + return true; + }); + + return true; +} + +bool oxen_service_nodes_sufficient_contribution_HF19::generate(std::vector &events) +{ + auto hard_forks = oxen_generate_hard_fork_table(cryptonote::network_version_19); + oxen_chain_generator gen(events, hard_forks); + + gen.add_blocks_until_version(hard_forks.back().version); + gen.add_mined_money_unlock_blocks(); + + const auto alice = gen.add_account(); + const auto tx0 = gen.create_and_add_tx(gen.first_miner_, alice.get_keys().m_account_address, MK_COINS(101)); + gen.create_and_add_next_block({tx0}); + gen.add_transfer_unlock_blocks(); + + uint64_t operator_portions = STAKING_PORTIONS_V1 / MAX_NUMBER_OF_CONTRIBUTORS_V1; + uint64_t operator_amount = service_nodes::get_staking_requirement(cryptonote::FAKECHAIN, hard_forks.back().height) / MAX_NUMBER_OF_CONTRIBUTORS_V1; + uint64_t remaining_portions = STAKING_PORTIONS_V1 - operator_portions; + uint64_t single_portion_illegal_HF18_legal_HF19 = remaining_portions / (MAX_NUMBER_OF_CONTRIBUTORS_V2 - 1); + uint64_t single_contributed_amount = (service_nodes::get_staking_requirement(cryptonote::FAKECHAIN, hard_forks.back().height) - operator_amount) / (MAX_NUMBER_OF_CONTRIBUTORS_V2 - 1); + uint64_t total_amount = operator_amount + single_contributed_amount; + cryptonote::keypair sn_keys{hw::get_device("default")}; + cryptonote::transaction register_tx = gen.create_registration_tx(gen.first_miner_, sn_keys, operator_portions); + gen.add_tx(register_tx); + gen.create_and_add_next_block({register_tx}); + + assert(single_contributed_amount == 0); + cryptonote::transaction stake = gen.create_and_add_staking_tx(sn_keys.pub, alice, single_contributed_amount); + gen.create_and_add_next_block({stake}); + + oxen_register_callback(events, "test_sufficient_stake_does_get_accepted", [sn_keys, total_amount](cryptonote::core &c, size_t ev_index) + { + DEFINE_TESTS_ERROR_CONTEXT("test_sufficient_stake_does_get_accepted"); + const auto sn_list = c.get_service_node_list_state({sn_keys.pub}); + CHECK_TEST_CONDITION(sn_list.size() == 1); + CHECK_TEST_CONDITION(sn_list[0].info->contributors.size() == 2); + + service_nodes::service_node_pubkey_info const &pubkey_info = sn_list[0]; + CHECK_EQ(pubkey_info.info->total_contributed, total_amount); + return true; + }); + + return true; +} + +bool oxen_service_nodes_insufficient_operator_contribution_HF19::generate(std::vector &events) +{ + auto hard_forks = oxen_generate_hard_fork_table(cryptonote::network_version_19); + oxen_chain_generator gen(events, hard_forks); + + gen.add_blocks_until_version(hard_forks.back().version); + gen.add_mined_money_unlock_blocks(); + + uint64_t operator_portions = STAKING_PORTIONS_V1 / MAX_NUMBER_OF_CONTRIBUTORS_V2; + uint64_t operator_amount = service_nodes::get_staking_requirement(cryptonote::FAKECHAIN, hard_forks.back().height) / MAX_NUMBER_OF_CONTRIBUTORS_V2; + cryptonote::keypair sn_keys{hw::get_device("default")}; + cryptonote::transaction register_tx = gen.create_registration_tx(gen.first_miner_, sn_keys, operator_portions); + gen.add_tx(register_tx); + gen.create_and_add_next_block({register_tx}); + + oxen_register_callback(events, "test_insufficient_operator_stake_does_not_get_accepted", [sn_keys, operator_amount](cryptonote::core &c, size_t ev_index) + { + DEFINE_TESTS_ERROR_CONTEXT("test_insufficient_operator_stake_does_not_get_accepted"); + const auto sn_list = c.get_service_node_list_state({sn_keys.pub}); + CHECK_TEST_CONDITION(sn_list.size() == 0); + return true; + }); + + return true; +} + static oxen_chain_generator setup_pulse_tests(std::vector &events) { auto hard_forks = oxen_generate_hard_fork_table(); diff --git a/tests/core_tests/oxen_tests.h b/tests/core_tests/oxen_tests.h index 6d80a1e6b..5ef328690 100644 --- a/tests/core_tests/oxen_tests.h +++ b/tests/core_tests/oxen_tests.h @@ -77,6 +77,9 @@ struct oxen_service_nodes_alt_quorums struct oxen_service_nodes_checkpoint_quorum_size : public test_chain_unit_base { bool generate(std::vector& events); }; struct oxen_service_nodes_gen_nodes : public test_chain_unit_base { bool generate(std::vector& events); }; struct oxen_service_nodes_insufficient_contribution : public test_chain_unit_base { bool generate(std::vector& events); }; +struct oxen_service_nodes_insufficient_contribution_HF18 : public test_chain_unit_base { bool generate(std::vector& events); }; +struct oxen_service_nodes_sufficient_contribution_HF19 : public test_chain_unit_base { bool generate(std::vector& events); }; +struct oxen_service_nodes_insufficient_operator_contribution_HF19 : public test_chain_unit_base { bool generate(std::vector& events); }; struct oxen_service_nodes_test_rollback : public test_chain_unit_base { bool generate(std::vector& events); }; struct oxen_service_nodes_test_swarms_basic : public test_chain_unit_base { bool generate(std::vector& events); }; struct oxen_pulse_invalid_validator_bitset : public test_chain_unit_base { bool generate(std::vector& events); }; diff --git a/tests/unit_tests/service_nodes.cpp b/tests/unit_tests/service_nodes.cpp index 765b6f78e..a770e1bc1 100644 --- a/tests/unit_tests/service_nodes.cpp +++ b/tests/unit_tests/service_nodes.cpp @@ -335,50 +335,50 @@ TEST(service_nodes, min_portions) uint8_t hf_version = cryptonote::network_version_9_service_nodes; // Test new contributors can *NOT* stake to a registration with under 25% of the total stake if there is more than 25% available. { - ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {0, STAKING_PORTIONS})); + ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {0, STAKING_PORTIONS_V1})); } { - const auto small = MIN_PORTIONS - 1; - const auto rest = STAKING_PORTIONS - small; + const auto small = MIN_PORTIONS_V2 - 1; + const auto rest = STAKING_PORTIONS_V1 - small; ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {small, rest})); } { /// TODO: fix this test - const auto small = MIN_PORTIONS - 1; - const auto rest = STAKING_PORTIONS - small - STAKING_PORTIONS / 2; - ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {STAKING_PORTIONS / 2, small, rest})); + const auto small = MIN_PORTIONS_V2 - 1; + const auto rest = STAKING_PORTIONS_V1 - small - STAKING_PORTIONS_V1 / 2; + ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {STAKING_PORTIONS_V1 / 2, small, rest})); } { - const auto small = MIN_PORTIONS - 1; - const auto rest = STAKING_PORTIONS - small - 2 * MIN_PORTIONS; - ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {MIN_PORTIONS, MIN_PORTIONS, small, rest})); + const auto small = MIN_PORTIONS_V2 - 1; + const auto rest = STAKING_PORTIONS_V1 - small - 2 * MIN_PORTIONS_V2; + ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {MIN_PORTIONS_V2, MIN_PORTIONS_V2, small, rest})); } // Test new contributors *CAN* stake as the last person with under 25% if there is less than 25% available. // Two contributers { - const auto large = 4 * (STAKING_PORTIONS / 5); - const auto rest = STAKING_PORTIONS - large; + const auto large = 4 * (STAKING_PORTIONS_V1 / 5); + const auto rest = STAKING_PORTIONS_V1 - large; bool result = service_nodes::check_service_node_portions(hf_version, {large, rest}); ASSERT_TRUE(result); } // Three contributers { - const auto half = STAKING_PORTIONS / 2 - 1; - const auto rest = STAKING_PORTIONS - 2 * half; + const auto half = STAKING_PORTIONS_V1 / 2 - 1; + const auto rest = STAKING_PORTIONS_V1 - 2 * half; bool result = service_nodes::check_service_node_portions(hf_version, {half, half, rest}); ASSERT_TRUE(result); } // Four contributers { - const auto third = STAKING_PORTIONS / 3 - 1; - const auto rest = STAKING_PORTIONS - 3 * third; + const auto third = STAKING_PORTIONS_V1 / 3 - 1; + const auto rest = STAKING_PORTIONS_V1 - 3 * third; bool result = service_nodes::check_service_node_portions(hf_version, {third, third, third, rest}); ASSERT_TRUE(result); } @@ -387,59 +387,59 @@ TEST(service_nodes, min_portions) hf_version = cryptonote::network_version_11_infinite_staking; { - ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {0, STAKING_PORTIONS})); + ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {0, STAKING_PORTIONS_V1})); } { - const auto small = MIN_PORTIONS - 1; - const auto rest = STAKING_PORTIONS - small; + const auto small = MIN_PORTIONS_V2 - 1; + const auto rest = STAKING_PORTIONS_V1 - small; ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {small, rest})); } { - const auto small = STAKING_PORTIONS / 8; - const auto rest = STAKING_PORTIONS - small - STAKING_PORTIONS / 2; - ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {STAKING_PORTIONS / 2, small, rest})); + const auto small = STAKING_PORTIONS_V1 / 8; + const auto rest = STAKING_PORTIONS_V1 - small - STAKING_PORTIONS_V1 / 2; + ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {STAKING_PORTIONS_V1 / 2, small, rest})); } { - const auto small = MIN_PORTIONS - 1; - const auto rest = STAKING_PORTIONS - small - 2 * MIN_PORTIONS; - ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {MIN_PORTIONS, MIN_PORTIONS, small, rest})); + const auto small = MIN_PORTIONS_V2 - 1; + const auto rest = STAKING_PORTIONS_V1 - small - 2 * MIN_PORTIONS_V2; + ASSERT_FALSE(service_nodes::check_service_node_portions(hf_version, {MIN_PORTIONS_V2, MIN_PORTIONS_V2, small, rest})); } // Test new contributors *CAN* stake as the last person with under 25% if there is less than 25% available. // Two contributers { - const auto large = 4 * (STAKING_PORTIONS / 5); - const auto rest = STAKING_PORTIONS - large; + const auto large = 4 * (STAKING_PORTIONS_V1 / 5); + const auto rest = STAKING_PORTIONS_V1 - large; bool result = service_nodes::check_service_node_portions(hf_version, {large, rest}); ASSERT_TRUE(result); } // Three contributers { - const auto half = STAKING_PORTIONS / 2 - 1; - const auto rest = STAKING_PORTIONS - 2 * half; + const auto half = STAKING_PORTIONS_V1 / 2 - 1; + const auto rest = STAKING_PORTIONS_V1 - 2 * half; bool result = service_nodes::check_service_node_portions(hf_version, {half, half, rest}); ASSERT_TRUE(result); } // Four contributers { - const auto third = STAKING_PORTIONS / 3 - 1; - const auto rest = STAKING_PORTIONS - 3 * third; + const auto third = STAKING_PORTIONS_V1 / 3 - 1; + const auto rest = STAKING_PORTIONS_V1 - 3 * third; bool result = service_nodes::check_service_node_portions(hf_version, {third, third, third, rest}); ASSERT_TRUE(result); } // New test for hf_v11: allow less than 25% stake in the presence of large existing contributions { - const auto large = STAKING_PORTIONS / 2; - const auto small_1 = STAKING_PORTIONS / 6; - const auto small_2 = STAKING_PORTIONS / 6; - const auto rest = STAKING_PORTIONS - large - small_1 - small_2; + const auto large = STAKING_PORTIONS_V1 / 2; + const auto small_1 = STAKING_PORTIONS_V1 / 6; + const auto small_2 = STAKING_PORTIONS_V1 / 6; + const auto rest = STAKING_PORTIONS_V1 - large - small_1 - small_2; bool result = service_nodes::check_service_node_portions(hf_version, {large, small_1, small_2, rest}); ASSERT_TRUE(result); } @@ -500,17 +500,10 @@ TEST(service_nodes, min_stake_amount) TEST(service_nodes, service_node_rewards_proportional_to_portions) { { - const auto reward_a = cryptonote::get_portion_of_reward(MIN_PORTIONS, COIN); - const auto reward_b = cryptonote::get_portion_of_reward(3 * MIN_PORTIONS, COIN); - ASSERT_TRUE(3 * reward_a == reward_b); + const auto reward_a = cryptonote::get_portion_of_reward(STAKING_PORTIONS_V1/2, COIN); + const auto reward_b = cryptonote::get_portion_of_reward(STAKING_PORTIONS_V1, COIN); + ASSERT_EQ(2 * reward_a, reward_b); } - - { - const auto reward_a = cryptonote::get_portion_of_reward(STAKING_PORTIONS/2, COIN); - const auto reward_b = cryptonote::get_portion_of_reward(STAKING_PORTIONS, COIN); - ASSERT_TRUE(2 * reward_a == reward_b); - } - } TEST(service_nodes, service_node_get_locked_key_image_unlock_height) diff --git a/tests/unit_tests/sqlite.cpp b/tests/unit_tests/sqlite.cpp index 5a8c136a8..abd1dec95 100644 --- a/tests/unit_tests/sqlite.cpp +++ b/tests/unit_tests/sqlite.cpp @@ -126,18 +126,18 @@ TEST(SQLITE, CalculateRewards) EXPECT_TRUE(multiple_rewards[2].amount == 68); // Check that 3 contributors receives their portion of the block reward when the operator takes a 10% fee - multiple_contributors.portions_for_operator = STAKING_PORTIONS/10; + multiple_contributors.portions_for_operator = STAKING_PORTIONS_V1/10; multiple_contributors.operator_address = first_address.address; block.reward = 1000; auto multiple_rewards_with_fee = sqliteDB.calculate_rewards(block.major_version, block.reward, multiple_contributors); // Operator gets 10% - EXPECT_TRUE(multiple_rewards_with_fee[0].amount == 99); - EXPECT_TRUE(tools::view_guts(multiple_rewards_with_fee[0].address_info.address) == tools::view_guts(first_address.address)); + EXPECT_EQ(multiple_rewards_with_fee[0].amount, 99); + EXPECT_EQ(tools::view_guts(multiple_rewards_with_fee[0].address_info.address), tools::view_guts(first_address.address)); // Contributors (including operator) receive the balance - EXPECT_TRUE(multiple_rewards_with_fee[1].amount == 297); - EXPECT_TRUE(tools::view_guts(multiple_rewards_with_fee[1].address_info.address) == tools::view_guts(first_address.address)); - EXPECT_TRUE(multiple_rewards_with_fee[2].amount == 297); - EXPECT_TRUE(tools::view_guts(multiple_rewards_with_fee[2].address_info.address) == tools::view_guts(second_address.address)); - EXPECT_TRUE(multiple_rewards_with_fee[3].amount == 306); - EXPECT_TRUE(tools::view_guts(multiple_rewards_with_fee[3].address_info.address) == tools::view_guts(third_address.address)); + EXPECT_EQ(multiple_rewards_with_fee[1].amount, 297); + EXPECT_EQ(tools::view_guts(multiple_rewards_with_fee[1].address_info.address), tools::view_guts(first_address.address)); + EXPECT_EQ(multiple_rewards_with_fee[2].amount, 297); + EXPECT_EQ(tools::view_guts(multiple_rewards_with_fee[2].address_info.address), tools::view_guts(second_address.address)); + EXPECT_EQ(multiple_rewards_with_fee[3].amount, 306); + EXPECT_EQ(tools::view_guts(multiple_rewards_with_fee[3].address_info.address), tools::view_guts(third_address.address)); } diff --git a/utils/local-devnet/commands/gettransaction.py b/utils/local-devnet/commands/gettransaction.py index 4c7a100a5..2232edcea 100755 --- a/utils/local-devnet/commands/gettransaction.py +++ b/utils/local-devnet/commands/gettransaction.py @@ -23,7 +23,7 @@ def instruct_daemon(method, params): parser = argparse.ArgumentParser(description='Get Block.') -parser.add_argument("--hash", help="An integer for the height to be queried", type=int) +parser.add_argument("--hash", help="An integer for the height to be queried", type=str) args = parser.parse_args() if not (args.hash): diff --git a/utils/local-devnet/daemons.py b/utils/local-devnet/daemons.py index 38925eef4..f56ceb6c5 100644 --- a/utils/local-devnet/daemons.py +++ b/utils/local-devnet/daemons.py @@ -374,10 +374,10 @@ class Wallet(RPCDaemon): if 'error' in r: raise RuntimeError("Failed to submit service node registration tx: {}".format(r['error']['message'])) - def register_sn_for_contributions(self, sn): + def register_sn_for_contributions(self, sn, cut, amount): r = sn.json_rpc("get_service_node_registration_cmd", { - "operator_cut": "10", - "contributions": [{"address": self.address(), "amount": 50000000000}], + "operator_cut": str(cut), + "contributions": [{"address": self.address(), "amount": amount}], "staking_requirement": 100000000000 }).json() if 'error' in r: @@ -387,10 +387,10 @@ class Wallet(RPCDaemon): if 'error' in r: raise RuntimeError("Failed to submit service node registration tx: {}".format(r['error']['message'])) - def contribute_to_sn(self, sn): + def contribute_to_sn(self, sn, amount): r = self.json_rpc("stake", { "destination": self.address(), - "amount": 50000000000, + "amount": amount, "service_node_key": sn.sn_key(), }).json() if 'error' in r: diff --git a/utils/local-devnet/service_node_network.py b/utils/local-devnet/service_node_network.py index 34cad943b..9aad09c4c 100755 --- a/utils/local-devnet/service_node_network.py +++ b/utils/local-devnet/service_node_network.py @@ -72,6 +72,14 @@ class SNNetwork: self.alice, self.bob, self.mike = self.wallets + self.extrawallets = [] + for name in range(9): + self.extrawallets.append(Wallet( + node=self.nodes[len(self.extrawallets) % len(self.nodes)], + name="extrawallet-"+str(name), + rpc_wallet=self.binpath+'/oxen-wallet-rpc', + datadir=datadir)) + # Interconnections for i in range(len(self.all_nodes)): for j in (2, 3, 5, 7, 11): @@ -105,8 +113,18 @@ class SNNetwork: w.refresh() vprint("Wallet {w.name} is ready: {a}".format(w=w, a=w.address())) + for w in self.extrawallets: + vprint("Starting new RPC wallet {w.name} at {w.listen_ip}:{w.rpc_port}".format(w=w)) + w.start() + for w in self.extrawallets: + w.ready() + w.refresh() + vprint("Wallet {w.name} is ready: {a}".format(w=w, a=w.address())) + for w in self.wallets: w.wait_for_json_rpc("refresh") + for w in self.extrawallets: + w.wait_for_json_rpc("refresh") configfile=self.datadir+'config.py' with open(configfile, 'w') as filetowrite: @@ -121,7 +139,7 @@ class SNNetwork: # of 18.9, which means each registration requires 6 inputs. Thus we need a bare minimum of # 6(N-5) blocks, plus the 30 lock time on coinbase TXes = 6N more blocks (after the initial # 5 registrations). - self.mine(100) + self.mine(200) vprint("Submitting first round of service node registrations: ", end="", flush=True) for sn in self.sns[0:5]: self.mike.register_sn(sn) @@ -145,7 +163,11 @@ class SNNetwork: self.print_wallet_balances() vprint("Mining 40 blocks (registrations + blink quorum lag) and waiting for nodes to sync") - self.sync_nodes(self.mine(40), timeout=120) + self.sync_nodes(self.mine(39), timeout=120) + for wallet in self.extrawallets: + self.mike.transfer(wallet, 11000000000) + self.sync_nodes(self.mine(1), timeout=120) + self.print_wallet_balances() @@ -156,6 +178,7 @@ class SNNetwork: all_service_nodes_proofed = lambda sn: all(x['quorumnet_port'] > 0 for x in sn.json_rpc("get_n_service_nodes", {"fields":{"quorumnet_port":True}}).json()['result']['service_node_states']) + vprint("Waiting for proofs to propagate: ", end="", flush=True) for sn in self.sns: wait_for(lambda: all_service_nodes_proofed(sn), timeout=120) @@ -170,11 +193,13 @@ class SNNetwork: # self.bob.register_sn(self.sns[-1]) # This commented out code will register the last SN through Bobs wallet (Has not done any others) - # and also get alice to contribute 50% of the node with a 10% operator fee - self.bob.register_sn_for_contributions(self.sns[-1]) - self.sync_nodes(self.mine(5), timeout=120) - self.alice.contribute_to_sn(self.sns[-1]) - self.sync_nodes(self.mine(2), timeout=120) + # and also get 9 other wallets to contribute the rest of the node with a 10% operator fee + self.bob.register_sn_for_contributions(self.sns[-1], 10, 28000000000) + self.sync_nodes(self.mine(10), timeout=120) + self.print_wallet_balances() + for wallet in self.extrawallets: + wallet.contribute_to_sn(self.sns[-1], 8000000000) + self.sync_nodes(self.mine(1), timeout=120) time.sleep(10) for sn in self.sns: sn.send_uptime_proof() @@ -264,6 +289,10 @@ class SNNetwork: b = w.balances(refresh=True) vprint(" {:5s}: {:.9f} (total) with {:.9f} (unlocked)".format( w.name, b[0] * 1e-9, b[1] * 1e-9)) + for w in self.extrawallets: + b = w.balances(refresh=True) + vprint(" {:5s}: {:.9f} (total) with {:.9f} (unlocked)".format( + w.name, b[0] * 1e-9, b[1] * 1e-9)) def __del__(self):