oxen-core/src/cryptonote_core/service_node_rules.cpp

274 lines
9.2 KiB
C++

#include "common/string_util.h"
#include "cryptonote_config.h"
#include "cryptonote_basic/hardfork.h"
#include "common/oxen.h"
#include "epee/int-util.h"
#include <oxenc/endian.h>
#include <limits>
#include <vector>
#include <boost/lexical_cast.hpp>
#include <cfenv>
#include "oxen_economy.h"
#include "service_node_rules.h"
using cryptonote::hf;
namespace service_nodes {
uint64_t get_staking_requirement(cryptonote::network_type nettype, hf hardfork)
{
assert(hardfork >= hf::hf16_pulse);
return nettype == cryptonote::network_type::MAINNET
? oxen::STAKING_REQUIREMENT
: oxen::STAKING_REQUIREMENT_TESTNET;
}
// TODO(oxen): Move to oxen_economy, this will also need access to oxen::exp2
uint64_t get_staking_requirement(cryptonote::network_type nettype, uint64_t height)
{
if (nettype != cryptonote::network_type::MAINNET)
return oxen::STAKING_REQUIREMENT_TESTNET;
if (is_hard_fork_at_least(nettype, hf::hf16_pulse, height))
return oxen::STAKING_REQUIREMENT;
if (is_hard_fork_at_least(nettype, hf::hf13_enforce_checkpoints, height))
{
constexpr int64_t heights[] = {
385824,
429024,
472224,
515424,
558624,
601824,
645024,
};
constexpr int64_t lsr[] = {
20458'380815527,
19332'319724305,
18438'564443912,
17729'190407764,
17166'159862153,
16719'282221956,
16364'595203882,
};
assert(static_cast<int64_t>(height) >= heights[0]);
constexpr uint64_t LAST_HEIGHT = heights[oxen::array_count(heights) - 1];
constexpr uint64_t LAST_REQUIREMENT = lsr [oxen::array_count(lsr) - 1];
if (height >= LAST_HEIGHT)
return LAST_REQUIREMENT;
size_t i = 0;
for (size_t index = 1; index < oxen::array_count(heights); index++)
{
if (heights[index] > static_cast<int64_t>(height))
{
i = (index - 1);
break;
}
}
int64_t H = height;
int64_t result = lsr[i] + (H - heights[i]) * ((lsr[i + 1] - lsr[i]) / (heights[i + 1] - heights[i]));
return static_cast<uint64_t>(result);
}
uint64_t hardfork_height = 101250;
if (height < hardfork_height) height = hardfork_height;
uint64_t height_adjusted = height - hardfork_height;
uint64_t base = 0, variable = 0;
std::fesetround(FE_TONEAREST);
if (is_hard_fork_at_least(nettype, hf::hf11_infinite_staking, height))
{
base = 15000 * oxen::COIN;
variable = (25007.0 * oxen::COIN) / oxen::exp2(height_adjusted/129600.0);
}
else
{
base = 10000 * oxen::COIN;
variable = (35000.0 * oxen::COIN) / oxen::exp2(height_adjusted/129600.0);
}
uint64_t result = base + variable;
return result;
}
uint64_t portions_to_amount(uint64_t portions, uint64_t staking_requirement)
{
return mul128_div64(staking_requirement, portions, cryptonote::old::STAKING_PORTIONS);
}
bool check_service_node_portions(hf hf_version, const std::vector<std::pair<cryptonote::account_public_address, uint64_t>>& portions)
{
// When checking portion we always use HF18 rules, even on HF19, because a registration actually
// generated under HF19+ won't get here.
if (hf_version == hf::hf19_reward_batching)
hf_version = hf::hf18;
else if (hf_version > hf::hf19_reward_batching)
{
LOG_PRINT_L1("Registration tx rejected: portions-based registrations not permitted after HF19");
return false;
}
if (portions.size() > oxen::MAX_CONTRIBUTORS_V1) {
LOG_PRINT_L1("Registration tx rejected: too many contributors (" << portions.size() << " > " << oxen::MAX_CONTRIBUTORS_V1 << ")");
return false;
}
uint64_t reserved = 0;
uint64_t remaining = cryptonote::old::STAKING_PORTIONS;
for (size_t i = 0; i < portions.size(); ++i)
{
const uint64_t min_portions = get_min_node_contribution(hf_version, cryptonote::old::STAKING_PORTIONS, reserved, i);
if (portions[i].second < min_portions) {
LOG_PRINT_L1("Registration tx rejected: portion " << i << " too small (" << portions[i].second << " < " << min_portions << ")");
return false;
}
if (portions[i].second > remaining) {
LOG_PRINT_L1("Registration tx rejected: portion " << i << " exceeds available portions");
return false;
}
reserved += portions[i].second;
remaining -= portions[i].second;
}
return true;
}
bool check_service_node_stakes(hf hf_version, cryptonote::network_type nettype, uint64_t staking_requirement, const std::vector<std::pair<cryptonote::account_public_address, uint64_t>>& stakes)
{
if (hf_version < hf::hf19_reward_batching) {
LOG_PRINT_L1("Registration tx rejected: amount-based registrations not accepted before HF19");
return false; // OXEN-based registrations not accepted before HF19
}
if (stakes.size() > oxen::MAX_CONTRIBUTORS_HF19) {
LOG_PRINT_L1("Registration tx rejected: too many contributors (" << stakes.size() << " > " << oxen::MAX_CONTRIBUTORS_HF19 << ")");
return false;
}
const auto operator_requirement = nettype == cryptonote::network_type::MAINNET
? oxen::MINIMUM_OPERATOR_CONTRIBUTION
: oxen::MINIMUM_OPERATOR_CONTRIBUTION_TESTNET;
uint64_t reserved = 0;
uint64_t remaining = staking_requirement;
for (size_t i = 0; i < stakes.size(); i++) {
const uint64_t min_stake = i == 0 ? operator_requirement : get_min_node_contribution(hf_version, staking_requirement, reserved, i);
if (stakes[i].second < min_stake) {
LOG_PRINT_L1("Registration tx rejected: stake " << i << " too small (" << stakes[i].second << " < " << min_stake << ")");
return false;
}
if (stakes[i].second > remaining) {
LOG_PRINT_L1("Registration tx rejected: stake " << i << " (" << stakes[i].second << ") exceeds available remaining stake (" << remaining << ")");
return false;
}
reserved += stakes[i].second;
remaining -= stakes[i].second;
}
return true;
}
crypto::hash generate_request_stake_unlock_hash(uint32_t nonce)
{
static_assert(sizeof(crypto::hash) == 8 * sizeof(uint32_t) && alignof(crypto::hash) >= alignof(uint32_t));
crypto::hash result;
oxenc::host_to_little_inplace(nonce);
for (size_t i = 0; i < 8; i++)
reinterpret_cast<uint32_t*>(result.data)[i] = nonce;
return result;
}
uint64_t get_locked_key_image_unlock_height(cryptonote::network_type nettype, uint64_t node_register_height, uint64_t curr_height)
{
uint64_t blocks_to_lock = staking_num_lock_blocks(nettype);
uint64_t result = curr_height + (blocks_to_lock / 2);
return result;
}
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 / oxen::MAX_CONTRIBUTORS_V1);
}
uint64_t get_max_node_contribution(hf version, uint64_t staking_requirement, uint64_t total_reserved)
{
if (version >= hf::hf16_pulse)
return (staking_requirement - total_reserved) * cryptonote::MAXIMUM_ACCEPTABLE_STAKE::num
/ cryptonote::MAXIMUM_ACCEPTABLE_STAKE::den;
return std::numeric_limits<uint64_t>::max();
}
uint64_t get_min_node_contribution(hf version, uint64_t staking_requirement, uint64_t total_reserved, size_t num_contributions)
{
if (version < hf::hf11_infinite_staking)
return get_min_node_contribution_pre_v11(staking_requirement, total_reserved);
const uint64_t needed = staking_requirement - total_reserved;
const size_t max_contributors = version >= hf::hf19_reward_batching ? oxen::MAX_CONTRIBUTORS_HF19 : oxen::MAX_CONTRIBUTORS_V1;
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;
}
uint64_t get_min_node_contribution_in_portions(hf version, uint64_t staking_requirement, uint64_t total_reserved, size_t num_contributions)
{
uint64_t atomic_amount = get_min_node_contribution(version, staking_requirement, total_reserved, num_contributions);
uint64_t result = (atomic_amount == UINT64_MAX) ? UINT64_MAX : (get_portions_to_make_amount(staking_requirement, atomic_amount));
return result;
}
uint64_t get_portions_to_make_amount(uint64_t staking_requirement, uint64_t amount, uint64_t max_portions)
{
uint64_t lo, hi, resulthi, resultlo;
lo = mul128(amount, max_portions, &hi);
if (lo > UINT64_MAX - (staking_requirement - 1))
hi++;
lo += staking_requirement-1;
div128_64(hi, lo, staking_requirement, &resulthi, &resultlo);
return resultlo;
}
std::optional<double> parse_fee_percent(std::string_view fee)
{
if (tools::ends_with(fee, "%"))
fee.remove_suffix(1);
double percent;
try {
percent = boost::lexical_cast<double>(fee);
} catch(...) {
return std::nullopt;
}
if (percent < 0 || percent > 100)
return std::nullopt;
return percent;
}
uint16_t percent_to_basis_points(std::string percent_string) {
const auto percent = parse_fee_percent(percent_string);
if (!percent)
throw invalid_registration{"could not parse fee percent"};
if(*percent < 0.0 || *percent > 100.0)
throw invalid_registration{"fee percent out of bounds"};
auto basis_points = static_cast<uint16_t>(std::lround(*percent / 100.0 * cryptonote::STAKING_FEE_BASIS));
if (*percent == 100.0)
basis_points = cryptonote::STAKING_FEE_BASIS;
return basis_points;
}
} // namespace service_nodes