Increase max number of contributors to 10

This requires the operator to still contribute 25% of the service node
but another 9 nodes will be allowed to stake to the node makeing 10
contributors total rather than our previous 4.
This commit is contained in:
Sean Darcy 2022-02-14 11:19:15 +11:00 committed by Jason Rhinelander
parent 3ac7e55056
commit 9edc2bb52d
No known key found for this signature in database
GPG Key ID: C4992CE7A88D4262
20 changed files with 307 additions and 131 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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;
}

View File

@ -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<uint64_t>(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<uint64_t, MAX_NUMBER_OF_CONTRIBUTORS> excess_portions;
std::array<uint64_t, MAX_NUMBER_OF_CONTRIBUTORS> min_contributions;
std::array<uint64_t, MAX_NUMBER_OF_CONTRIBUTORS_V2> excess_portions;
std::array<uint64_t, MAX_NUMBER_OF_CONTRIBUTORS_V2> 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;

View File

@ -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}};
}

View File

@ -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<uint64_t>& 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;

View File

@ -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);

View File

@ -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<std::string> addresses;
std::vector<uint64_t> 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<double>(STAKING_PORTIONS));
state.operator_fee_portions * 100.0 / static_cast<double>(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)",

View File

@ -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++)
{

View File

@ -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};

View File

@ -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<oxen_service_node_contribution, 3> const &contributors = {},
int num_contributors = 0) const;

View File

@ -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);

View File

@ -2980,14 +2980,20 @@ bool oxen_service_nodes_insufficient_contribution::generate(std::vector<test_eve
gen.add_blocks_until_version(hard_forks.back().version);
gen.add_mined_money_unlock_blocks();
uint64_t operator_portions = STAKING_PORTIONS / 2;
uint64_t remaining_portions = STAKING_PORTIONS - operator_portions;
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 / 2;
uint64_t remaining_portions = STAKING_PORTIONS_V1 - operator_portions;
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();
cryptonote::transaction stake = gen.create_and_add_staking_tx(sn_keys.pub, gen.first_miner_, MK_COINS(1));
cryptonote::transaction stake = gen.create_and_add_staking_tx(sn_keys.pub, alice, MK_COINS(1));
gen.create_and_add_next_block({stake});
oxen_register_callback(events, "test_insufficient_stake_does_not_get_accepted", [sn_keys](cryptonote::core &c, size_t ev_index)
@ -3004,6 +3010,118 @@ bool oxen_service_nodes_insufficient_contribution::generate(std::vector<test_eve
return true;
}
bool oxen_service_nodes_insufficient_contribution_HF18::generate(std::vector<test_event_entry> &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<test_event_entry> &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<test_event_entry> &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<test_event_entry> &events)
{
auto hard_forks = oxen_generate_hard_fork_table();

View File

@ -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<test_event_entry>& events); };
struct oxen_service_nodes_gen_nodes : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_service_nodes_insufficient_contribution : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_service_nodes_insufficient_contribution_HF18 : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_service_nodes_sufficient_contribution_HF19 : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_service_nodes_insufficient_operator_contribution_HF19 : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_service_nodes_test_rollback : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_service_nodes_test_swarms_basic : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };
struct oxen_pulse_invalid_validator_bitset : public test_chain_unit_base { bool generate(std::vector<test_event_entry>& events); };

View File

@ -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)

View File

@ -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));
}

View File

@ -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):

View File

@ -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:

View File

@ -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):