diff --git a/contracts/eosio.system/CMakeLists.txt b/contracts/eosio.system/CMakeLists.txt index 3f9090846..33f7d486e 100644 --- a/contracts/eosio.system/CMakeLists.txt +++ b/contracts/eosio.system/CMakeLists.txt @@ -4,6 +4,7 @@ add_contract(eosio.system eosio.system ${CMAKE_CURRENT_SOURCE_DIR}/src/exchange_state.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/native.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/producer_pay.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/rentbw.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/rex.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/voting.cpp ) diff --git a/contracts/eosio.system/include/eosio.system/eosio.system.hpp b/contracts/eosio.system/include/eosio.system/eosio.system.hpp index 3cfce3f42..0b5d6c21c 100644 --- a/contracts/eosio.system/include/eosio.system/eosio.system.hpp +++ b/contracts/eosio.system/include/eosio.system/eosio.system.hpp @@ -38,6 +38,8 @@ namespace eosiosystem { using eosio::time_point_sec; using eosio::unsigned_int; + inline constexpr int64_t rentbw_frac = 1'000'000'000'000'000ll; // 1.0 = 10^15 + template static inline auto has_field( F flags, E field ) -> std::enable_if_t< std::is_integral_v && std::is_unsigned_v && @@ -85,6 +87,7 @@ namespace eosiosystem { * - Users can bid on premium names. * - A resource exchange system (REX) allows token holders to lend their tokens, * and users to rent CPU and Network resources in return for a market-determined fee. + * - A resource market separate from REX: `rentbw` */ // A name bid, which consists of: @@ -339,8 +342,8 @@ namespace eosiosystem { // - `version` defaulted to zero, // - `last_dist_time` the last time proceeds from renting, ram fees, and name bids were added to the rex pool, // - `pending_bucket_time` timestamp of the pending 12-hour return bucket, - // - `oldest_bucket_time` cached timestamp of the oldest 12-hour return bucket, - // - `pending_bucket_proceeds` proceeds in the pending 12-hour return bucket, + // - `oldest_bucket_time` cached timestamp of the oldest 12-hour return bucket, + // - `pending_bucket_proceeds` proceeds in the pending 12-hour return bucket, // - `current_rate_of_increase` the current rate per dist_interval at which proceeds are added to the rex pool, // - `proceeds` the maximum amount of proceeds that can be added to the rex pool at any given time struct [[eosio::table,eosio::contract("eosio.system")]] rex_return_pool { @@ -364,7 +367,7 @@ namespace eosiosystem { // `rex_return_buckets` structure underlying the rex return buckets table. A rex return buckets table is defined by: // - `version` defaulted to zero, - // - `return_buckets` buckets of proceeds accumulated in 12-hour intervals + // - `return_buckets` buckets of proceeds accumulated in 12-hour intervals struct [[eosio::table,eosio::contract("eosio.system")]] rex_return_buckets { uint8_t version = 0; std::map return_buckets; @@ -465,6 +468,123 @@ namespace eosiosystem { asset stake_change; }; + struct rentbw_config_resource { + std::optional current_weight_ratio; // Immediately set weight_ratio to this amount. 1x = 10^15. 0.01x = 10^13. + // Do not specify to preserve the existing setting or use the default; + // this avoids sudden price jumps. For new chains which don't need + // to gradually phase out staking and REX, 0.01x (10^13) is a good + // value for both current_weight_ratio and target_weight_ratio. + std::optional target_weight_ratio; // Linearly shrink weight_ratio to this amount. 1x = 10^15. 0.01x = 10^13. + // Do not specify to preserve the existing setting or use the default. + std::optional assumed_stake_weight; // Assumed stake weight for ratio calculations. Use the sum of total + // staked and total rented by REX at the time the rentbw market + // is first activated. Do not specify to preserve the existing + // setting (no default exists); this avoids sudden price jumps. + // For new chains which don't need to phase out staking and REX, + // 10^12 is probably a good value. + std::optional target_timestamp; // Stop automatic weight_ratio shrinkage at this time. Once this + // time hits, weight_ratio will be target_weight_ratio. Ignored + // if current_weight_ratio == target_weight_ratio. Do not specify + // this to preserve the existing setting (no default exists). + std::optional exponent; // Exponent of resource price curve. Must be >= 1. Do not specify + // to preserve the existing setting or use the default. + std::optional decay_secs; // Number of seconds for the gap between adjusted resource + // utilization and instantaneous resource utilization to shrink + // by 63%. Do not specify to preserve the existing setting or + // use the default. + std::optional min_price; // Fee needed to rent the entire resource market weight at the + // minimum price. For example, this could be set to 0.005% of + // total token supply. Do not specify to preserve the existing + // setting or use the default. + std::optional max_price; // Fee needed to rent the entire resource market weight at the + // maximum price. For example, this could be set to 10% of total + // token supply. Do not specify to preserve the existing + // setting (no default exists). + + EOSLIB_SERIALIZE( rentbw_config_resource, (current_weight_ratio)(target_weight_ratio)(assumed_stake_weight) + (target_timestamp)(exponent)(decay_secs)(min_price)(max_price) ) + }; + + struct rentbw_config { + rentbw_config_resource net; // NET market configuration + rentbw_config_resource cpu; // CPU market configuration + std::optional rent_days; // `rentbw` `days` argument must match this. Do not specify to preserve the + // existing setting or use the default. + std::optional min_rent_fee; // Rental fees below this amount are rejected. Do not specify to preserve the + // existing setting (no default exists). + + EOSLIB_SERIALIZE( rentbw_config, (net)(cpu)(rent_days)(min_rent_fee) ) + }; + + struct rentbw_state_resource { + static constexpr double default_exponent = 2.0; // Exponent of 2.0 means that the price to rent a + // tiny amount of resources increases linearly + // with utilization. + static constexpr uint32_t default_decay_secs = 1 * seconds_per_day; // 1 day; if 100% of bandwidth resources are in a + // single loan, then, assuming no further renting, + // 1 day after it expires the adjusted utilization + // will be at approximately 37% and after 3 days + // the adjusted utilization will be less than 5%. + + uint8_t version = 0; + int64_t weight = 0; // resource market weight. calculated; varies over time. + // 1 represents the same amount of resources as 1 + // satoshi of SYS staked. + int64_t weight_ratio = 0; // resource market weight ratio: + // assumed_stake_weight / (assumed_stake_weight + weight). + // calculated; varies over time. 1x = 10^15. 0.01x = 10^13. + int64_t assumed_stake_weight = 0; // Assumed stake weight for ratio calculations. + int64_t initial_weight_ratio = rentbw_frac; // Initial weight_ratio used for linear shrinkage. + int64_t target_weight_ratio = rentbw_frac / 100; // Linearly shrink the weight_ratio to this amount. + time_point_sec initial_timestamp = {}; // When weight_ratio shrinkage started + time_point_sec target_timestamp = {}; // Stop automatic weight_ratio shrinkage at this time. Once this + // time hits, weight_ratio will be target_weight_ratio. + double exponent = default_exponent; // Exponent of resource price curve. + uint32_t decay_secs = default_decay_secs; // Number of seconds for the gap between adjusted resource + // utilization and instantaneous utilization to shrink by 63%. + asset min_price = {}; // Fee needed to rent the entire resource market weight at + // the minimum price (defaults to 0). + asset max_price = {}; // Fee needed to rent the entire resource market weight at + // the maximum price. + int64_t utilization = 0; // Instantaneous resource utilization. This is the current + // amount sold. utilization <= weight. + int64_t adjusted_utilization = 0; // Adjusted resource utilization. This is >= utilization and + // <= weight. It grows instantly but decays exponentially. + time_point_sec utilization_timestamp = {}; // When adjusted_utilization was last updated + }; + + struct [[eosio::table("rent.state"),eosio::contract("eosio.system")]] rentbw_state { + static constexpr uint32_t default_rent_days = 30; // 30 day resource rentals + + uint8_t version = 0; + rentbw_state_resource net = {}; // NET market state + rentbw_state_resource cpu = {}; // CPU market state + uint32_t rent_days = default_rent_days; // `rentbw` `days` argument must match this. + asset min_rent_fee = {}; // Rental fees below this amount are rejected + + uint64_t primary_key()const { return 0; } + }; + + typedef eosio::singleton<"rent.state"_n, rentbw_state> rentbw_state_singleton; + + struct [[eosio::table("rentbw.order"),eosio::contract("eosio.system")]] rentbw_order { + uint8_t version = 0; + uint64_t id; + name owner; + int64_t net_weight; + int64_t cpu_weight; + time_point_sec expires; + + uint64_t primary_key()const { return id; } + uint64_t by_owner()const { return owner.value; } + uint64_t by_expires()const { return expires.utc_seconds; } + }; + + typedef eosio::multi_index< "rentbw.order"_n, rentbw_order, + indexed_by<"byowner"_n, const_mem_fun>, + indexed_by<"byexpires"_n, const_mem_fun> + > rentbw_order_table; + /** * The EOSIO system contract. The EOSIO system contract governs ram market, voters, producers, global state. */ @@ -501,6 +621,7 @@ namespace eosiosystem { static constexpr eosio::name names_account{"eosio.names"_n}; static constexpr eosio::name saving_account{"eosio.saving"_n}; static constexpr eosio::name rex_account{"eosio.rex"_n}; + static constexpr eosio::name reserv_account{"eosio.reserv"_n}; static constexpr eosio::name null_account{"eosio.null"_n}; static constexpr symbol ramcore_symbol = symbol(symbol_code("RAMCORE"), 4); static constexpr symbol ram_symbol = symbol(symbol_code("RAM"), 0); @@ -1101,6 +1222,36 @@ namespace eosiosystem { [[eosio::action]] void setinflation( int64_t annual_rate, int64_t inflation_pay_factor, int64_t votepay_factor ); + /** + * Configure the `rentbw` market. The market becomes available the first time this + * action is invoked. + */ + [[eosio::action]] + void configrentbw( rentbw_config& args ); + + /** + * Process rentbw queue and update state. Action does not execute anything related to a specific user. + * + * @param user - any account can execute this action + * @param max - number of queue items to process + */ + [[eosio::action]] + void rentbwexec( const name& user, uint16_t max ); + + /** + * Rent NET and CPU + * + * @param payer - the resource buyer + * @param receiver - the resource receiver + * @param days - number of days of resource availability. Must match market configuration. + * @param net_frac - fraction of net (100% = 10^15) managed by this market + * @param cpu_frac - fraction of cpu (100% = 10^15) managed by this market + * @param max_payment - the maximum amount `payer` is willing to pay. Tokens are withdrawn from + * `payer`'s token balance. + */ + [[eosio::action]] + void rentbw( const name& payer, const name& receiver, uint32_t days, int64_t net_frac, int64_t cpu_frac, const asset& max_payment ); + using init_action = eosio::action_wrapper<"init"_n, &system_contract::init>; using setacctram_action = eosio::action_wrapper<"setacctram"_n, &system_contract::setacctram>; using setacctnet_action = eosio::action_wrapper<"setacctnet"_n, &system_contract::setacctnet>; @@ -1146,6 +1297,9 @@ namespace eosiosystem { using setalimits_action = eosio::action_wrapper<"setalimits"_n, &system_contract::setalimits>; using setparams_action = eosio::action_wrapper<"setparams"_n, &system_contract::setparams>; using setinflation_action = eosio::action_wrapper<"setinflation"_n, &system_contract::setinflation>; + using configrentbw_action = eosio::action_wrapper<"configrentbw"_n, &system_contract::configrentbw>; + using rentbwexec_action = eosio::action_wrapper<"rentbwexec"_n, &system_contract::rentbwexec>; + using rentbw_action = eosio::action_wrapper<"rentbw"_n, &system_contract::rentbw>; private: // Implementation details: @@ -1170,7 +1324,7 @@ namespace eosiosystem { const char* error_msg = "must vote for at least 21 producers or for a proxy before buying REX" )const; rex_order_outcome fill_rex_order( const rex_balance_table::const_iterator& bitr, const asset& rex ); asset update_rex_account( const name& owner, const asset& proceeds, const asset& unstake_quant, bool force_vote_update = false ); - void channel_to_rex( const name& from, const asset& amount ); + void channel_to_rex( const name& from, const asset& amount, bool required = false ); void channel_namebid_to_rex( const int64_t highest_bid ); template int64_t rent_rex( T& table, const name& from, const name& receiver, const asset& loan_payment, const asset& loan_fund ); @@ -1245,6 +1399,13 @@ namespace eosiosystem { }; registration<&system_contract::update_rex_stake> vote_stake_updater{ this }; + + // defined in rentbw.cpp + void adjust_resources(name payer, name account, symbol core_symbol, int64_t net_delta, int64_t cpu_delta, bool must_not_be_managed = false); + void process_rentbw_queue( + time_point_sec now, symbol core_symbol, rentbw_state& state, + rentbw_order_table& orders, uint32_t max_items, int64_t& net_delta_available, + int64_t& cpu_delta_available); }; } diff --git a/contracts/eosio.system/src/rentbw.cpp b/contracts/eosio.system/src/rentbw.cpp new file mode 100644 index 000000000..195f53d37 --- /dev/null +++ b/contracts/eosio.system/src/rentbw.cpp @@ -0,0 +1,393 @@ +#include +#include +#include +#include + +namespace eosiosystem { + +void update_weight(time_point_sec now, rentbw_state_resource& res, int64_t& delta_available); + +/** + * @pre now >= res.utilization_timestamp + * @post res.utilization <= new res.adjusted_utilization + * @post if res.utilization < old res.adjusted_utilization, then new res.adjusted_utilization <= old res.adjusted_utilization + * @post if res.utilization >= old res.adjusted_utilization, then new res.adjusted_utilization == res.utilization + */ +void update_utilization(time_point_sec now, rentbw_state_resource& res); + +void system_contract::adjust_resources(name payer, name account, symbol core_symbol, int64_t net_delta, + int64_t cpu_delta, bool must_not_be_managed) { + if (!net_delta && !cpu_delta) + return; + + user_resources_table totals_tbl(get_self(), account.value); + auto tot_itr = totals_tbl.find(account.value); + if (tot_itr == totals_tbl.end()) { + tot_itr = totals_tbl.emplace(payer, [&](auto& tot) { + tot.owner = account; + tot.net_weight = asset{ net_delta, core_symbol }; + tot.cpu_weight = asset{ cpu_delta, core_symbol }; + }); + } else { + totals_tbl.modify(tot_itr, same_payer, [&](auto& tot) { + tot.net_weight.amount += net_delta; + tot.cpu_weight.amount += cpu_delta; + }); + } + check(0 <= tot_itr->net_weight.amount, "insufficient staked total net bandwidth"); + check(0 <= tot_itr->cpu_weight.amount, "insufficient staked total cpu bandwidth"); + + { + bool ram_managed = false; + bool net_managed = false; + bool cpu_managed = false; + + auto voter_itr = _voters.find(account.value); + if (voter_itr != _voters.end()) { + ram_managed = has_field(voter_itr->flags1, voter_info::flags1_fields::ram_managed); + net_managed = has_field(voter_itr->flags1, voter_info::flags1_fields::net_managed); + cpu_managed = has_field(voter_itr->flags1, voter_info::flags1_fields::cpu_managed); + } + + if (must_not_be_managed) + eosio::check(!net_managed && !cpu_managed, "something is managed which shouldn't be"); + + if (!(net_managed && cpu_managed)) { + int64_t ram_bytes, net, cpu; + get_resource_limits(account, ram_bytes, net, cpu); + set_resource_limits( + account, ram_managed ? ram_bytes : std::max(tot_itr->ram_bytes + ram_gift_bytes, ram_bytes), + net_managed ? net : tot_itr->net_weight.amount, cpu_managed ? cpu : tot_itr->cpu_weight.amount); + } + } + + if (tot_itr->is_empty()) { + totals_tbl.erase(tot_itr); + } +} // system_contract::adjust_resources + +void system_contract::process_rentbw_queue(time_point_sec now, symbol core_symbol, rentbw_state& state, + rentbw_order_table& orders, uint32_t max_items, int64_t& net_delta_available, + int64_t& cpu_delta_available) { + update_utilization(now, state.net); + update_utilization(now, state.cpu); + auto idx = orders.get_index<"byexpires"_n>(); + while (max_items--) { + auto it = idx.begin(); + if (it == idx.end() || it->expires > now) + break; + net_delta_available += it->net_weight; + cpu_delta_available += it->cpu_weight; + adjust_resources(get_self(), it->owner, core_symbol, -it->net_weight, -it->cpu_weight); + idx.erase(it); + } + state.net.utilization -= net_delta_available; + state.cpu.utilization -= cpu_delta_available; + update_weight(now, state.net, net_delta_available); + update_weight(now, state.cpu, cpu_delta_available); +} + +void update_weight(time_point_sec now, rentbw_state_resource& res, int64_t& delta_available) { + if (now >= res.target_timestamp) { + res.weight_ratio = res.target_weight_ratio; + } else { + res.weight_ratio = res.initial_weight_ratio + // + int128_t(res.target_weight_ratio - res.initial_weight_ratio) * + (now.utc_seconds - res.initial_timestamp.utc_seconds) / + (res.target_timestamp.utc_seconds - res.initial_timestamp.utc_seconds); + } + int64_t new_weight = res.assumed_stake_weight * int128_t(rentbw_frac) / res.weight_ratio - res.assumed_stake_weight; + delta_available += new_weight - res.weight; + res.weight = new_weight; +} + +void update_utilization(time_point_sec now, rentbw_state_resource& res) { + if (now <= res.utilization_timestamp) return; + + if (res.utilization >= res.adjusted_utilization) { + res.adjusted_utilization = res.utilization; + } else { + int64_t diff = res.adjusted_utilization - res.utilization; + int64_t delta = diff * std::exp(-double(now.utc_seconds - res.utilization_timestamp.utc_seconds) / double(res.decay_secs)); + delta = std::clamp( delta, 0ll, diff); + res.adjusted_utilization = res.utilization + delta; + } + res.utilization_timestamp = now; +} + +void system_contract::configrentbw(rentbw_config& args) { + require_auth(get_self()); + time_point_sec now = eosio::current_time_point(); + auto core_symbol = get_core_symbol(); + rentbw_state_singleton state_sing{ get_self(), 0 }; + auto state = state_sing.get_or_default(); + + eosio::check(eosio::is_account(reserv_account), "eosio.reserv account must first be created"); + + int64_t net_delta_available = 0; + int64_t cpu_delta_available = 0; + if (state_sing.exists()) { + update_utilization(now, state.net); + update_utilization(now, state.cpu); + update_weight(now, state.net, net_delta_available); + update_weight(now, state.cpu, cpu_delta_available); + } else { + state.net.utilization_timestamp = now; + state.cpu.utilization_timestamp = now; + } + + auto is_default_asset = []( const eosio::asset& a ) -> bool { + return a.amount == 0 && a.symbol == symbol{}; + }; + + auto update = [&](auto& state, auto& args) { + if (!args.current_weight_ratio) { + if (state.weight_ratio) { + *args.current_weight_ratio = state.weight_ratio; + } else { + *args.current_weight_ratio = state.initial_weight_ratio; + } + } + + if (!args.target_weight_ratio) { + *args.target_weight_ratio = state.target_weight_ratio; + } + + if (!args.assumed_stake_weight) { + eosio::check(state.assumed_stake_weight != 0, "assumed_stake_weight does not have a default value"); + *args.assumed_stake_weight = state.assumed_stake_weight; + } + + if (*args.current_weight_ratio == *args.target_weight_ratio) { + *args.target_timestamp = now; + } else { + if (!args.target_timestamp) { + eosio::check(state.target_timestamp.utc_seconds != 0, "target_timestamp does not have a default value"); + *args.target_timestamp = state.target_timestamp; + } + eosio::check(*args.target_timestamp > now, "target_timestamp must be in the future"); + } + + if (!args.exponent) { + *args.exponent = state.exponent; + } + + if (!args.decay_secs) { + *args.decay_secs = state.decay_secs; + } + + if (!args.max_price) { + eosio::check(!is_default_asset(state.max_price), "max_price does not have a default value"); + *args.max_price = state.max_price; + } + + if (!args.min_price) { + if (is_default_asset(state.min_price)) { + *args.min_price = *args.max_price; // just to copy symbol of max_price + args.min_price->amount = 0; // min_price has a default of zero. + } else { + *args.min_price = state.min_price; + } + } + + eosio::check(*args.current_weight_ratio > 0, "current_weight_ratio is too small"); + eosio::check(*args.current_weight_ratio <= rentbw_frac, "current_weight_ratio is too large"); + eosio::check(*args.target_weight_ratio > 0, "target_weight_ratio is too small"); + eosio::check(*args.target_weight_ratio <= *args.current_weight_ratio, "weight can't grow over time"); + eosio::check(*args.assumed_stake_weight >= 1, + "assumed_stake_weight must be at least 1; a much larger value is recommended"); + eosio::check(*args.assumed_stake_weight * int128_t(rentbw_frac) / *args.target_weight_ratio <= + std::numeric_limits::max(), + "assumed_stake_weight/target_weight_ratio is too large"); + eosio::check(*args.exponent >= 1.0, "exponent must be >= 1"); + eosio::check(*args.decay_secs >= 1, "decay_secs must be >= 1"); + eosio::check(args.max_price->symbol == core_symbol, "max_price doesn't match core symbol"); + eosio::check(args.max_price->amount > 0, "max_price must be positive"); + eosio::check(args.min_price->symbol == core_symbol, "min_price doesn't match core symbol"); + eosio::check(args.min_price->amount >= 0, "min_price must be non-negative"); + eosio::check(args.min_price->amount <= args.max_price->amount, "min_price cannot exceed max_price"); + if (*args.exponent == 1.0) { + eosio::check(args.min_price->amount == args.max_price->amount, "min_price and max_price must be the same if the exponent is 1"); + } + + state.assumed_stake_weight = *args.assumed_stake_weight; + state.initial_weight_ratio = *args.current_weight_ratio; + state.target_weight_ratio = *args.target_weight_ratio; + state.initial_timestamp = now; + state.target_timestamp = *args.target_timestamp; + state.exponent = *args.exponent; + state.decay_secs = *args.decay_secs; + state.min_price = *args.min_price; + state.max_price = *args.max_price; + }; + + if (!args.rent_days) { + *args.rent_days = state.rent_days; + } + + if (!args.min_rent_fee) { + eosio::check(!is_default_asset(state.min_rent_fee), "min_rent_fee does not have a default value"); + *args.min_rent_fee = state.min_rent_fee; + } + + eosio::check(*args.rent_days > 0, "rent_days must be > 0"); + eosio::check(args.min_rent_fee->symbol == core_symbol, "min_rent_fee doesn't match core symbol"); + eosio::check(args.min_rent_fee->amount > 0, "min_rent_fee must be positive"); + + state.rent_days = *args.rent_days; + state.min_rent_fee = *args.min_rent_fee; + + update(state.net, args.net); + update(state.cpu, args.cpu); + + update_weight(now, state.net, net_delta_available); + update_weight(now, state.cpu, cpu_delta_available); + eosio::check(state.net.weight >= state.net.utilization, "weight can't shrink below utilization"); + eosio::check(state.cpu.weight >= state.cpu.utilization, "weight can't shrink below utilization"); + state.net.adjusted_utilization = std::min(state.net.adjusted_utilization, state.net.weight); + state.cpu.adjusted_utilization = std::min(state.cpu.adjusted_utilization, state.cpu.weight); + + adjust_resources(get_self(), reserv_account, core_symbol, net_delta_available, cpu_delta_available, true); + state_sing.set(state, get_self()); +} // system_contract::configrentbw + +/** + * @pre 0 <= state.min_price.amount <= state.max_price.amount + * @pre 0 < state.max_price.amount + * @pre 1.0 <= state.exponent + * @pre 0 <= state.utilization <= state.adjusted_utilization <= state.weight + * @pre 0 <= utilization_increase <= (state.weight - state.utilization) + */ +int64_t calc_rentbw_fee(const rentbw_state_resource& state, int64_t utilization_increase) { + if( utilization_increase <= 0 ) return 0; + + // Let p(u) = price as a function of the utilization fraction u which is defined for u in [0.0, 1.0]. + // Let f(u) = integral of the price function p(x) from x = 0.0 to x = u, again defined for u in [0.0, 1.0]. + + // In particular we choose f(u) = min_price * u + ((max_price - min_price) / exponent) * (u ^ exponent). + // And so p(u) = min_price + (max_price - min_price) * (u ^ (exponent - 1.0)). + + // Returns f(double(end_utilization)/state.weight) - f(double(start_utilization)/state.weight) which is equivalent to + // the integral of p(x) from x = double(start_utilization)/state.weight to x = double(end_utilization)/state.weight. + // @pre 0 <= start_utilization <= end_utilization <= state.weight + auto price_integral_delta = [&state](int64_t start_utilization, int64_t end_utilization) -> double { + double coefficient = (state.max_price.amount - state.min_price.amount) / state.exponent; + double start_u = double(start_utilization) / state.weight; + double end_u = double(end_utilization) / state.weight; + return state.min_price.amount * end_u - state.min_price.amount * start_u + + coefficient * std::pow(end_u, state.exponent) - coefficient * std::pow(start_u, state.exponent); + }; + + // Returns p(double(utilization)/state.weight). + // @pre 0 <= utilization <= state.weight + auto price_function = [&state](int64_t utilization) -> double { + double price = state.min_price.amount; + // state.exponent >= 1.0, therefore the exponent passed into std::pow is >= 0.0. + // Since the exponent passed into std::pow could be 0.0 and simultaneously so could double(utilization)/state.weight, + // the safest thing to do is handle that as a special case explicitly rather than relying on std::pow to return 1.0 + // instead of triggering a domain error. + double new_exponent = state.exponent - 1.0; + if (new_exponent <= 0.0) { + return state.max_price.amount; + } else { + price += (state.max_price.amount - state.min_price.amount) * std::pow(double(utilization) / state.weight, new_exponent); + } + + return price; + }; + + double fee = 0.0; + int64_t start_utilization = state.utilization; + int64_t end_utilization = start_utilization + utilization_increase; + + if (start_utilization < state.adjusted_utilization) { + fee += price_function(state.adjusted_utilization) * + std::min(utilization_increase, state.adjusted_utilization - start_utilization) / state.weight; + start_utilization = state.adjusted_utilization; + } + + if (start_utilization < end_utilization) { + fee += price_integral_delta(start_utilization, end_utilization); + } + + return std::ceil(fee); +} + +void system_contract::rentbwexec(const name& user, uint16_t max) { + require_auth(user); + rentbw_state_singleton state_sing{ get_self(), 0 }; + rentbw_order_table orders{ get_self(), 0 }; + eosio::check(state_sing.exists(), "rentbw hasn't been initialized"); + auto state = state_sing.get(); + time_point_sec now = eosio::current_time_point(); + auto core_symbol = get_core_symbol(); + + int64_t net_delta_available = 0; + int64_t cpu_delta_available = 0; + process_rentbw_queue(now, core_symbol, state, orders, max, net_delta_available, cpu_delta_available); + + adjust_resources(get_self(), reserv_account, core_symbol, net_delta_available, cpu_delta_available, true); + state_sing.set(state, get_self()); +} + +void system_contract::rentbw(const name& payer, const name& receiver, uint32_t days, int64_t net_frac, int64_t cpu_frac, + const asset& max_payment) { + require_auth(payer); + rentbw_state_singleton state_sing{ get_self(), 0 }; + rentbw_order_table orders{ get_self(), 0 }; + eosio::check(state_sing.exists(), "rentbw hasn't been initialized"); + auto state = state_sing.get(); + time_point_sec now = eosio::current_time_point(); + auto core_symbol = get_core_symbol(); + eosio::check(max_payment.symbol == core_symbol, "max_payment doesn't match core symbol"); + eosio::check(days == state.rent_days, "days doesn't match configuration"); + eosio::check(net_frac >= 0, "net_frac can't be negative"); + eosio::check(cpu_frac >= 0, "cpu_frac can't be negative"); + eosio::check(net_frac <= rentbw_frac, "net can't be more than 100%"); + eosio::check(cpu_frac <= rentbw_frac, "cpu can't be more than 100%"); + + int64_t net_delta_available = 0; + int64_t cpu_delta_available = 0; + process_rentbw_queue(now, core_symbol, state, orders, 2, net_delta_available, cpu_delta_available); + + eosio::asset fee{ 0, core_symbol }; + auto process = [&](int64_t frac, int64_t& amount, rentbw_state_resource& state) { + if (!frac) + return; + amount = int128_t(frac) * state.weight / rentbw_frac; + eosio::check(state.weight, "market doesn't have resources available"); + eosio::check(state.utilization + amount <= state.weight, "market doesn't have enough resources available"); + int64_t f = calc_rentbw_fee(state, amount); + eosio::check(f > 0, "calculated fee is below minimum; try renting more"); + fee.amount += f; + state.utilization += amount; + }; + + int64_t net_amount = 0; + int64_t cpu_amount = 0; + process(net_frac, net_amount, state.net); + process(cpu_frac, cpu_amount, state.cpu); + if (fee > max_payment) { + std::string error_msg = "max_payment is less than calculated fee: "; + error_msg += fee.to_string(); + eosio::check(false, error_msg); + } + eosio::check(fee >= state.min_rent_fee, "calculated fee is below minimum; try renting more"); + + orders.emplace(payer, [&](auto& order) { + order.id = orders.available_primary_key(); + order.owner = receiver; + order.net_weight = net_amount; + order.cpu_weight = cpu_amount; + order.expires = now + eosio::days(days); + }); + net_delta_available -= net_amount; + cpu_delta_available -= cpu_amount; + + adjust_resources(payer, receiver, core_symbol, net_amount, cpu_amount, true); + adjust_resources(get_self(), reserv_account, core_symbol, net_delta_available, cpu_delta_available, true); + channel_to_rex(payer, fee, true); + state_sing.set(state, get_self()); +} + +} // namespace eosiosystem diff --git a/contracts/eosio.system/src/rex.cpp b/contracts/eosio.system/src/rex.cpp index 8e9d881f3..dee9c0ab8 100644 --- a/contracts/eosio.system/src/rex.cpp +++ b/contracts/eosio.system/src/rex.cpp @@ -916,8 +916,9 @@ namespace eosiosystem { * * @param from - account from which asset is transfered to REX pool * @param amount - amount of tokens to be transfered + * @param required - if true, asserts when the system is not configured to channel fees into REX */ - void system_contract::channel_to_rex( const name& from, const asset& amount ) + void system_contract::channel_to_rex( const name& from, const asset& amount, bool required ) { #if CHANNEL_RAM_AND_NAMEBID_FEES_TO_REX if ( rex_available() ) { @@ -926,8 +927,10 @@ namespace eosiosystem { token::transfer_action transfer_act{ token_account, { from, active_permission } }; transfer_act.send( from, rex_account, amount, std::string("transfer from ") + from.to_string() + " to eosio.rex" ); + return; } #endif + eosio::check( !required, "can't channel fees to rex" ); } /** diff --git a/tests/eosio.rentbw_tests.cpp b/tests/eosio.rentbw_tests.cpp new file mode 100644 index 000000000..115bfa838 --- /dev/null +++ b/tests/eosio.rentbw_tests.cpp @@ -0,0 +1,858 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "eosio.system_tester.hpp" + +inline constexpr int64_t rentbw_frac = 1'000'000'000'000'000ll; // 1.0 = 10^15 +inline constexpr int64_t stake_weight = 100'000'000'0000ll; // 10^12 + +struct rentbw_config_resource { + fc::optional current_weight_ratio = {}; + fc::optional target_weight_ratio = {}; + fc::optional assumed_stake_weight = {}; + fc::optional target_timestamp = {}; + fc::optional exponent = {}; + fc::optional decay_secs = {}; + fc::optional min_price = {}; + fc::optional max_price = {}; +}; +FC_REFLECT(rentbw_config_resource, // + (current_weight_ratio)(target_weight_ratio)(assumed_stake_weight)(target_timestamp) // + (exponent)(decay_secs)(min_price)(max_price)) + +struct rentbw_config { + rentbw_config_resource net = {}; + rentbw_config_resource cpu = {}; + fc::optional rent_days = {}; + fc::optional min_rent_fee = {}; +}; +FC_REFLECT(rentbw_config, (net)(cpu)(rent_days)(min_rent_fee)) + +struct rentbw_state_resource { + uint8_t version; + int64_t weight; + int64_t weight_ratio; + int64_t assumed_stake_weight; + int64_t initial_weight_ratio; + int64_t target_weight_ratio; + time_point_sec initial_timestamp; + time_point_sec target_timestamp; + double exponent; + uint32_t decay_secs; + asset min_price; + asset max_price; + int64_t utilization; + int64_t adjusted_utilization; + time_point_sec utilization_timestamp; +}; +FC_REFLECT(rentbw_state_resource, // + (version)(weight)(weight_ratio)(assumed_stake_weight)(initial_weight_ratio)(target_weight_ratio) // + (initial_timestamp)(target_timestamp)(exponent)(decay_secs)(min_price)(max_price)(utilization) // + (adjusted_utilization)(utilization_timestamp)) + +struct rentbw_state { + uint8_t version; + rentbw_state_resource net; + rentbw_state_resource cpu; + uint32_t rent_days; + asset min_rent_fee; +}; +FC_REFLECT(rentbw_state, (version)(net)(cpu)(rent_days)(min_rent_fee)) + +using namespace eosio_system; + +struct rentbw_tester : eosio_system_tester { + + rentbw_tester() { create_accounts_with_resources({ N(eosio.reserv) }); } + + void start_rex() { + create_account_with_resources(N(rexholder111), config::system_account_name, core_sym::from_string("1.0000"), + false); + transfer(config::system_account_name, N(rexholder111), core_sym::from_string("1001.0000")); + BOOST_REQUIRE_EQUAL("", stake(N(rexholder111), N(rexholder111), core_sym::from_string("500.0000"), + core_sym::from_string("500.0000"))); + create_account_with_resources(N(proxyaccount), config::system_account_name, core_sym::from_string("1.0000"), + false, core_sym::from_string("500.0000"), core_sym::from_string("500.0000")); + BOOST_REQUIRE_EQUAL("", + push_action(N(proxyaccount), N(regproxy), mvo()("proxy", "proxyaccount")("isproxy", true))); + BOOST_REQUIRE_EQUAL("", vote(N(rexholder111), {}, N(proxyaccount))); + BOOST_REQUIRE_EQUAL("", push_action(N(rexholder111), N(deposit), + mvo()("owner", "rexholder111")("amount", asset::from_string("1.0000 TST")))); + BOOST_REQUIRE_EQUAL("", push_action(N(rexholder111), N(buyrex), + mvo()("from", "rexholder111")("amount", asset::from_string("1.0000 TST")))); + } + + template + rentbw_config make_config(F f) { + rentbw_config config; + + config.net.current_weight_ratio = rentbw_frac; + config.net.target_weight_ratio = rentbw_frac / 100; + config.net.assumed_stake_weight = stake_weight; + config.net.target_timestamp = control->head_block_time() + fc::days(100); + config.net.exponent = 2; + config.net.decay_secs = fc::days(1).to_seconds(); + config.net.min_price = asset::from_string("0.0000 TST"); + config.net.max_price = asset::from_string("1000000.0000 TST"); + + config.cpu.current_weight_ratio = rentbw_frac; + config.cpu.target_weight_ratio = rentbw_frac / 100; + config.cpu.assumed_stake_weight = stake_weight; + config.cpu.target_timestamp = control->head_block_time() + fc::days(100); + config.cpu.exponent = 2; + config.cpu.decay_secs = fc::days(1).to_seconds(); + config.cpu.min_price = asset::from_string("0.0000 TST"); + config.cpu.max_price = asset::from_string("1000000.0000 TST"); + + config.rent_days = 30; + config.min_rent_fee = asset::from_string("1.0000 TST"); + + f(config); + return config; + } + + rentbw_config make_config() { + return make_config([](auto&) {}); + } + + template + rentbw_config make_default_config(F f) { + rentbw_config config; + f(config); + return config; + } + + action_result configbw(const rentbw_config& config) { + // Verbose solution needed to work around bug in abi_serializer that fails if optional values aren't explicitly + // specified with a null value. + + auto optional_to_variant = []( const auto& v ) -> fc::variant { + return (!v ? fc::variant() : fc::variant(*v)); + }; + + auto resource_conf_vo = [&optional_to_variant](const rentbw_config_resource& c ) { + return mvo("current_weight_ratio", optional_to_variant(c.current_weight_ratio)) + ("target_weight_ratio", optional_to_variant(c.target_weight_ratio)) + ("assumed_stake_weight", optional_to_variant(c.assumed_stake_weight)) + ("target_timestamp", optional_to_variant(c.target_timestamp)) + ("exponent", optional_to_variant(c.exponent)) + ("decay_secs", optional_to_variant(c.decay_secs)) + ("min_price", optional_to_variant(c.min_price)) + ("max_price", optional_to_variant(c.max_price)) + ; + }; + + auto conf = mvo("net", resource_conf_vo(config.net)) + ("cpu", resource_conf_vo(config.cpu)) + ("rent_days", optional_to_variant(config.rent_days)) + ("min_rent_fee", optional_to_variant(config.min_rent_fee)) + ; + + //idump((fc::json::to_pretty_string(conf))); + return push_action(config::system_account_name, N(configrentbw), mvo()("args", std::move(conf))); + + // If abi_serializer worked correctly, the following is all that would be needed: + //return push_action(config::system_account_name, N(configrentbw), mvo()("args", config)); + } + + action_result rentbwexec(name user, uint16_t max) { + return push_action(user, N(rentbwexec), mvo()("user", user)("max", max)); + } + + action_result rentbw(const name& payer, const name& receiver, uint32_t days, int64_t net_frac, int64_t cpu_frac, + const asset& max_payment) { + return push_action(payer, N(rentbw), + mvo()("payer", payer)("receiver", receiver)("days", days)("net_frac", net_frac)( + "cpu_frac", cpu_frac)("max_payment", max_payment)); + } + + rentbw_state get_state() { + vector data = get_row_by_account(config::system_account_name, {}, N(rent.state), N(rent.state)); + return fc::raw::unpack(data); + } + + struct account_info { + int64_t ram = 0; + int64_t net = 0; + int64_t cpu = 0; + asset liquid; + }; + + account_info get_account_info(account_name acc) { + account_info info; + control->get_resource_limits_manager().get_account_limits(acc, info.ram, info.net, info.cpu); + info.liquid = get_balance(acc); + return info; + }; + + void check_rentbw(const name& payer, const name& receiver, uint32_t days, int64_t net_frac, int64_t cpu_frac, + const asset& expected_fee, int64_t expected_net, int64_t expected_cpu) { + auto before_payer = get_account_info(payer); + auto before_receiver = get_account_info(receiver); + auto before_reserve = get_account_info(N(eosio.reserv)); + auto before_state = get_state(); + BOOST_REQUIRE_EQUAL("", rentbw(payer, receiver, days, net_frac, cpu_frac, expected_fee)); + auto after_payer = get_account_info(payer); + auto after_receiver = get_account_info(receiver); + auto after_reserve = get_account_info(N(eosio.reserv)); + auto after_state = get_state(); + + if (false) { + ilog("before_state.net.assumed_stake_weight: ${x}", ("x", before_state.net.assumed_stake_weight)); + ilog("before_state.net.weight_ratio: ${x}", + ("x", before_state.net.weight_ratio / double(rentbw_frac))); + ilog("before_state.net.assumed_stake_weight: ${x}", ("x", before_state.net.assumed_stake_weight)); + ilog("before_state.net.weight: ${x}", ("x", before_state.net.weight)); + + ilog("before_receiver.net: ${x}", ("x", before_receiver.net)); + ilog("after_receiver.net: ${x}", ("x", after_receiver.net)); + ilog("after_receiver.net - before_receiver.net: ${x}", ("x", after_receiver.net - before_receiver.net)); + ilog("expected_net: ${x}", ("x", expected_net)); + ilog("before_payer.liquid - after_payer.liquid: ${x}", ("x", before_payer.liquid - after_payer.liquid)); + ilog("expected_fee: ${x}", ("x", expected_fee)); + + ilog("before_reserve.net: ${x}", ("x", before_reserve.net)); + ilog("after_reserve.net: ${x}", ("x", after_reserve.net)); + ilog("before_reserve.cpu: ${x}", ("x", before_reserve.cpu)); + ilog("after_reserve.cpu: ${x}", ("x", after_reserve.cpu)); + } + + if (payer != receiver) { + BOOST_REQUIRE_EQUAL(before_payer.ram, after_payer.ram); + BOOST_REQUIRE_EQUAL(before_payer.net, after_payer.net); + BOOST_REQUIRE_EQUAL(before_payer.cpu, after_payer.cpu); + BOOST_REQUIRE_EQUAL(before_receiver.liquid, after_receiver.liquid); + } + BOOST_REQUIRE_EQUAL(before_receiver.ram, after_receiver.ram); + BOOST_REQUIRE_EQUAL(after_receiver.net - before_receiver.net, expected_net); + BOOST_REQUIRE_EQUAL(after_receiver.cpu - before_receiver.cpu, expected_cpu); + BOOST_REQUIRE_EQUAL(before_payer.liquid - after_payer.liquid, expected_fee); + + BOOST_REQUIRE_EQUAL(before_reserve.net - after_reserve.net, expected_net); + BOOST_REQUIRE_EQUAL(before_reserve.cpu - after_reserve.cpu, expected_cpu); + BOOST_REQUIRE_EQUAL(after_state.net.utilization - before_state.net.utilization, expected_net); + BOOST_REQUIRE_EQUAL(after_state.cpu.utilization - before_state.cpu.utilization, expected_cpu); + } +}; + +template +bool near(A a, B b, D delta) { + if (abs(a - b) <= delta) + return true; + elog("near: ${a} ${b}", ("a", a)("b", b)); + return false; +} + +BOOST_AUTO_TEST_SUITE(eosio_system_rentbw_tests) + +BOOST_FIXTURE_TEST_CASE(config_tests, rentbw_tester) try { + BOOST_REQUIRE_EQUAL("missing authority of eosio", + push_action(N(alice1111111), N(configrentbw), mvo()("args", make_config()))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("rentbw hasn't been initialized"), rentbwexec(N(alice1111111), 10)); + + BOOST_REQUIRE_EQUAL(wasm_assert_msg("rent_days must be > 0"), + configbw(make_config([&](auto& c) { c.rent_days = 0; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("min_rent_fee doesn't match core symbol"), configbw(make_config([&](auto& c) { + c.min_rent_fee = asset::from_string("1000000.000 TST"); + }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("min_rent_fee does not have a default value"), + configbw(make_config([&](auto& c) { c.min_rent_fee = {}; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("min_rent_fee must be positive"), + configbw(make_config([&](auto& c) { c.min_rent_fee = asset::from_string("0.0000 TST"); }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("min_rent_fee must be positive"), + configbw(make_config([&](auto& c) { c.min_rent_fee = asset::from_string("-1.0000 TST"); }))); + + // net assertions + BOOST_REQUIRE_EQUAL(wasm_assert_msg("current_weight_ratio is too large"), + configbw(make_config([](auto& c) { c.net.current_weight_ratio = rentbw_frac + 1; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("assumed_stake_weight/target_weight_ratio is too large"), + configbw(make_config([](auto& c) { + c.net.assumed_stake_weight = 100000; + c.net.target_weight_ratio = 10; + }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("weight can't grow over time"), + configbw(make_config([](auto& c) { c.net.target_weight_ratio = rentbw_frac + 1; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("assumed_stake_weight does not have a default value"), + configbw(make_config([](auto& c) { c.net.assumed_stake_weight = {}; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("assumed_stake_weight must be at least 1; a much larger value is recommended"), + configbw(make_config([](auto& c) { c.net.assumed_stake_weight = 0; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("target_timestamp does not have a default value"), + configbw(make_config([&](auto& c) { c.net.target_timestamp = {}; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("target_timestamp must be in the future"), + configbw(make_config([&](auto& c) { c.net.target_timestamp = control->head_block_time(); }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("target_timestamp must be in the future"), configbw(make_config([&](auto& c) { + c.net.target_timestamp = control->head_block_time() - fc::seconds(1); + }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("exponent must be >= 1"), + configbw(make_config([&](auto& c) { c.net.exponent = .999; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("decay_secs must be >= 1"), + configbw(make_config([&](auto& c) { c.net.decay_secs = 0; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("max_price does not have a default value"), + configbw(make_config([&](auto& c) { c.net.max_price = {}; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("max_price doesn't match core symbol"), configbw(make_config([&](auto& c) { + c.net.max_price = asset::from_string("1000000.000 TST"); + }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("max_price must be positive"), + configbw(make_config([&](auto& c) { c.net.max_price = asset::from_string("0.0000 TST"); }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("max_price must be positive"), + configbw(make_config([&](auto& c) { c.net.max_price = asset::from_string("-1.0000 TST"); }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("min_price doesn't match core symbol"), configbw(make_config([&](auto& c) { + c.net.min_price = asset::from_string("1000000.000 TST"); + }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("min_price must be non-negative"), + configbw(make_config([&](auto& c) { c.net.min_price = asset::from_string("-1.0000 TST"); }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("min_price cannot exceed max_price"), + configbw(make_config([&](auto& c) { + c.net.min_price = asset::from_string("3.0000 TST"); + c.net.max_price = asset::from_string("2.0000 TST"); + }))); + + // cpu assertions + BOOST_REQUIRE_EQUAL(wasm_assert_msg("current_weight_ratio is too large"), + configbw(make_config([](auto& c) { c.cpu.current_weight_ratio = rentbw_frac + 1; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("assumed_stake_weight/target_weight_ratio is too large"), + configbw(make_config([](auto& c) { + c.cpu.assumed_stake_weight = 100000; + c.cpu.target_weight_ratio = 10; + }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("weight can't grow over time"), + configbw(make_config([](auto& c) { c.cpu.target_weight_ratio = rentbw_frac + 1; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("assumed_stake_weight does not have a default value"), + configbw(make_config([](auto& c) { c.cpu.assumed_stake_weight = {}; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("assumed_stake_weight must be at least 1; a much larger value is recommended"), + configbw(make_config([](auto& c) { c.cpu.assumed_stake_weight = 0; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("target_timestamp does not have a default value"), + configbw(make_config([&](auto& c) { c.cpu.target_timestamp = {}; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("target_timestamp must be in the future"), + configbw(make_config([&](auto& c) { c.cpu.target_timestamp = control->head_block_time(); }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("target_timestamp must be in the future"), configbw(make_config([&](auto& c) { + c.cpu.target_timestamp = control->head_block_time() - fc::seconds(1); + }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("exponent must be >= 1"), + configbw(make_config([&](auto& c) { c.cpu.exponent = .999; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("decay_secs must be >= 1"), + configbw(make_config([&](auto& c) { c.cpu.decay_secs = 0; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("max_price does not have a default value"), + configbw(make_config([&](auto& c) { c.cpu.max_price = {}; }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("max_price doesn't match core symbol"), configbw(make_config([&](auto& c) { + c.cpu.max_price = asset::from_string("1000000.000 TST"); + }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("max_price must be positive"), + configbw(make_config([&](auto& c) { c.cpu.max_price = asset::from_string("0.0000 TST"); }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("max_price must be positive"), + configbw(make_config([&](auto& c) { c.cpu.max_price = asset::from_string("-1.0000 TST"); }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("min_price doesn't match core symbol"), configbw(make_config([&](auto& c) { + c.cpu.min_price = asset::from_string("1000000.000 TST"); + }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("min_price must be non-negative"), + configbw(make_config([&](auto& c) { c.cpu.min_price = asset::from_string("-1.0000 TST"); }))); + BOOST_REQUIRE_EQUAL(wasm_assert_msg("min_price cannot exceed max_price"), + configbw(make_config([&](auto& c) { + c.cpu.min_price = asset::from_string("3.0000 TST"); + c.cpu.max_price = asset::from_string("2.0000 TST"); + }))); +} // config_tests +FC_LOG_AND_RETHROW() + +BOOST_FIXTURE_TEST_CASE(weight_tests, rentbw_tester) try { + produce_block(); + + auto net_start = (rentbw_frac * 11) / 100; + auto net_target = (rentbw_frac * 1) / 100; + auto cpu_start = (rentbw_frac * 11) / 1000; + auto cpu_target = (rentbw_frac * 1) / 1000; + + BOOST_REQUIRE_EQUAL("", configbw(make_config([&](rentbw_config& config) { + config.net.current_weight_ratio = net_start; + config.net.target_weight_ratio = net_target; + config.net.assumed_stake_weight = stake_weight; + config.net.target_timestamp = control->head_block_time() + fc::days(10); + + config.cpu.current_weight_ratio = cpu_start; + config.cpu.target_weight_ratio = cpu_target; + config.cpu.assumed_stake_weight = stake_weight; + config.cpu.target_timestamp = control->head_block_time() + fc::days(20); + }))); + + int64_t net; + int64_t cpu; + + auto check_weight = [&] { + auto state = get_state(); + BOOST_REQUIRE(near( // + state.net.weight_ratio, // + int64_t(state.net.assumed_stake_weight * eosio::chain::int128_t(rentbw_frac) / + (state.net.weight + state.net.assumed_stake_weight)), + 10)); + }; + + for (int i = 0; i <= 6; ++i) { + if (i == 2) { + // Leaves config as-is, but may introduce slight rounding + produce_block(fc::days(1) - fc::milliseconds(500)); + BOOST_REQUIRE_EQUAL("", configbw({})); + } else if (i) { + produce_block(fc::days(1) - fc::milliseconds(500)); + BOOST_REQUIRE_EQUAL("", rentbwexec(config::system_account_name, 10)); + } + net = net_start + i * (net_target - net_start) / 10; + cpu = cpu_start + i * (cpu_target - cpu_start) / 20; + BOOST_REQUIRE(near(get_state().net.weight_ratio, net, 1)); + BOOST_REQUIRE(near(get_state().cpu.weight_ratio, cpu, 1)); + check_weight(); + } + + // Extend transition time + { + int i = 7; + produce_block(fc::days(1) - fc::milliseconds(500)); + BOOST_REQUIRE_EQUAL("", configbw(make_default_config([&](rentbw_config& config) { + config.net.target_timestamp = control->head_block_time() + fc::days(30); + config.cpu.target_timestamp = control->head_block_time() + fc::days(40); + }))); + net_start = net = net_start + i * (net_target - net_start) / 10; + cpu_start = cpu = cpu_start + i * (cpu_target - cpu_start) / 20; + BOOST_REQUIRE(near(get_state().net.weight_ratio, net, 1)); + BOOST_REQUIRE(near(get_state().cpu.weight_ratio, cpu, 1)); + check_weight(); + } + + for (int i = 0; i <= 5; ++i) { + if (i) { + produce_block(fc::days(1) - fc::milliseconds(500)); + BOOST_REQUIRE_EQUAL("", rentbwexec(config::system_account_name, 10)); + } + net = net_start + i * (net_target - net_start) / 30; + cpu = cpu_start + i * (cpu_target - cpu_start) / 40; + BOOST_REQUIRE(near(get_state().net.weight_ratio, net, 1)); + BOOST_REQUIRE(near(get_state().cpu.weight_ratio, cpu, 1)); + check_weight(); + } + + // Change target, keep existing transition time + { + int i = 6; + produce_block(fc::days(1) - fc::milliseconds(500)); + auto new_net_target = net_target / 10; + auto new_cpu_target = cpu_target / 20; + BOOST_REQUIRE_EQUAL("", configbw(make_default_config([&](rentbw_config& config) { + config.net.target_weight_ratio = new_net_target; + config.cpu.target_weight_ratio = new_cpu_target; + }))); + net_start = net = net_start + i * (net_target - net_start) / 30; + cpu_start = cpu = cpu_start + i * (cpu_target - cpu_start) / 40; + net_target = new_net_target; + cpu_target = new_cpu_target; + BOOST_REQUIRE(near(get_state().net.weight_ratio, net, 1)); + BOOST_REQUIRE(near(get_state().cpu.weight_ratio, cpu, 1)); + check_weight(); + } + + for (int i = 0; i <= 10; ++i) { + if (i) { + produce_block(fc::days(1) - fc::milliseconds(500)); + BOOST_REQUIRE_EQUAL("", rentbwexec(config::system_account_name, 10)); + } + net = net_start + i * (net_target - net_start) / (30 - 6); + cpu = cpu_start + i * (cpu_target - cpu_start) / (40 - 6); + BOOST_REQUIRE(near(get_state().net.weight_ratio, net, 1)); + BOOST_REQUIRE(near(get_state().cpu.weight_ratio, cpu, 1)); + check_weight(); + } + + // Move transition time to immediate future + { + produce_block(fc::days(1) - fc::milliseconds(500)); + BOOST_REQUIRE_EQUAL("", configbw(make_default_config([&](rentbw_config& config) { + config.net.target_timestamp = control->head_block_time() + fc::milliseconds(1000); + config.cpu.target_timestamp = control->head_block_time() + fc::milliseconds(1000); + }))); + produce_blocks(2); + } + + // Verify targets hold as time advances + for (int i = 0; i <= 10; ++i) { + BOOST_REQUIRE_EQUAL("", rentbwexec(config::system_account_name, 10)); + BOOST_REQUIRE(near(get_state().net.weight_ratio, net_target, 1)); + BOOST_REQUIRE(near(get_state().cpu.weight_ratio, cpu_target, 1)); + check_weight(); + produce_block(fc::days(1)); + } +} // weight_tests +FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_CASE(rent_tests) try { + { + rentbw_tester t; + t.produce_block(); + + BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("rentbw hasn't been initialized"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac / 4, rentbw_frac / 8, + asset::from_string("1.000 TST"))); + + BOOST_REQUIRE_EQUAL("", t.configbw(t.make_config([&](auto& config) { + config.net.current_weight_ratio = rentbw_frac; + config.net.target_weight_ratio = rentbw_frac; + config.net.assumed_stake_weight = stake_weight; + config.net.exponent = 1; + config.net.min_price = asset::from_string("1000000.0000 TST"); + config.net.max_price = asset::from_string("1000000.0000 TST"); + + config.cpu.current_weight_ratio = rentbw_frac; + config.cpu.target_weight_ratio = rentbw_frac; + config.cpu.assumed_stake_weight = stake_weight; + config.cpu.exponent = 1; + config.cpu.min_price = asset::from_string("1000000.0000 TST"); + config.cpu.max_price = asset::from_string("1000000.0000 TST"); + + config.rent_days = 30; + config.min_rent_fee = asset::from_string("1.0000 TST"); + }))); + + BOOST_REQUIRE_EQUAL( + t.wasm_assert_msg("max_payment doesn't match core symbol"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac, rentbw_frac, asset::from_string("1.000 TST"))); + BOOST_REQUIRE_EQUAL( + t.wasm_assert_msg("market doesn't have resources available"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, 0, rentbw_frac, asset::from_string("1.0000 TST"))); + BOOST_REQUIRE_EQUAL( + t.wasm_assert_msg("market doesn't have resources available"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac, 0, asset::from_string("1.0000 TST"))); + + BOOST_REQUIRE_EQUAL("", t.configbw(t.make_default_config([&](auto& config) { + // weight = stake_weight + config.net.current_weight_ratio = rentbw_frac/2; + config.net.target_weight_ratio = rentbw_frac/2; + + // weight = stake_weight + config.cpu.current_weight_ratio = rentbw_frac/2; + config.cpu.target_weight_ratio = rentbw_frac/2; + }))); + + auto net_weight = stake_weight; + auto cpu_weight = stake_weight; + + t.start_rex(); + t.create_account_with_resources(N(aaaaaaaaaaaa), config::system_account_name, core_sym::from_string("1.0000"), + false, core_sym::from_string("500.0000"), core_sym::from_string("500.0000")); + + // 10%, 20% + // (.1) * 1000000.0000 = 100000.0000 + // (.2) * 1000000.0000 = 200000.0000 + // total = 300000.0000 + t.transfer(config::system_account_name, N(aaaaaaaaaaaa), core_sym::from_string("300000.0000")); + t.check_rentbw(N(aaaaaaaaaaaa), N(aaaaaaaaaaaa), 30, rentbw_frac * .1, rentbw_frac * .2, + asset::from_string("300000.0000 TST"), net_weight * .1, cpu_weight * .2); + + // Start decay + t.produce_block(fc::days(30) - fc::milliseconds(500)); + BOOST_REQUIRE_EQUAL("", t.rentbwexec(config::system_account_name, 10)); + BOOST_REQUIRE(near(t.get_state().net.adjusted_utilization, .1 * net_weight, 0)); + BOOST_REQUIRE(near(t.get_state().cpu.adjusted_utilization, .2 * cpu_weight, 0)); + + // 2 days of decay from (10%, 20%) to (1.35%, 2.71%) + t.produce_block(fc::days(2) - fc::milliseconds(500)); + BOOST_REQUIRE_EQUAL("", t.rentbwexec(config::system_account_name, 10)); + BOOST_REQUIRE(near(t.get_state().net.adjusted_utilization, int64_t(.1 * net_weight * exp(-2)), + int64_t(.1 * net_weight * exp(-2)) / 1000)); + BOOST_REQUIRE(near(t.get_state().cpu.adjusted_utilization, int64_t(.2 * cpu_weight * exp(-2)), + int64_t(.2 * cpu_weight * exp(-2)) / 1000)); + + // 2%, 2% + // (0.0135 + 0.02 - 0.0135) * 1000000.0000 = 20000.0000 + // (.02) * 1000000.0000 = 20000.0000 + // total = 40000.0000 + t.transfer(config::system_account_name, N(aaaaaaaaaaaa), core_sym::from_string("40000.0001")); + t.check_rentbw(N(aaaaaaaaaaaa), N(aaaaaaaaaaaa), 30, rentbw_frac * .02, rentbw_frac * .02, + asset::from_string("40000.0001 TST"), net_weight * .02, cpu_weight * .02); + } + + auto init = [](auto& t, bool rex) { + t.produce_block(); + BOOST_REQUIRE_EQUAL("", t.configbw(t.make_config([&](auto& config) { + // weight = stake_weight * 3 + config.net.current_weight_ratio = rentbw_frac / 4; + config.net.target_weight_ratio = rentbw_frac / 4; + config.net.assumed_stake_weight = stake_weight; + config.net.exponent = 2; + config.net.max_price = asset::from_string("2000000.0000 TST"); + + // weight = stake_weight * 4 / 2 + config.cpu.current_weight_ratio = rentbw_frac / 5; + config.cpu.target_weight_ratio = rentbw_frac / 5; + config.cpu.assumed_stake_weight = stake_weight / 2; + config.cpu.exponent = 3; + config.cpu.max_price = asset::from_string("6000000.0000 TST"); + + config.rent_days = 30; + config.min_rent_fee = asset::from_string("1.0000 TST"); + }))); + + if (rex) + t.start_rex(); + + t.create_account_with_resources(N(aaaaaaaaaaaa), config::system_account_name, core_sym::from_string("1.0000"), + false, core_sym::from_string("500.0000"), core_sym::from_string("500.0000")); + t.create_account_with_resources(N(bbbbbbbbbbbb), config::system_account_name, core_sym::from_string("1.0000"), + false, core_sym::from_string("500.0000"), core_sym::from_string("500.0000")); + }; + auto net_weight = stake_weight * 3; + auto cpu_weight = stake_weight * 4 / 2; + + { + rentbw_tester t; + init(t, false); + BOOST_REQUIRE_EQUAL( + t.wasm_assert_msg("days doesn't match configuration"), // + t.rentbw(N(bob111111111), N(alice1111111), 20, rentbw_frac, rentbw_frac, asset::from_string("1.0000 TST"))); + BOOST_REQUIRE_EQUAL( // + t.wasm_assert_msg("net_frac can't be negative"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, -rentbw_frac, rentbw_frac, + asset::from_string("1.0000 TST"))); + BOOST_REQUIRE_EQUAL( // + t.wasm_assert_msg("cpu_frac can't be negative"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac, -rentbw_frac, + asset::from_string("1.0000 TST"))); + BOOST_REQUIRE_EQUAL( // + t.wasm_assert_msg("net can't be more than 100%"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac + 1, rentbw_frac, + asset::from_string("1.0000 TST"))); + BOOST_REQUIRE_EQUAL( // + t.wasm_assert_msg("cpu can't be more than 100%"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac, rentbw_frac + 1, + asset::from_string("1.0000 TST"))); + BOOST_REQUIRE_EQUAL( + t.wasm_assert_msg("max_payment is less than calculated fee: 3000000.0000 TST"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac, rentbw_frac, asset::from_string("1.0000 TST"))); + BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("can't channel fees to rex"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac, rentbw_frac, + asset::from_string("3000000.0000 TST"))); + } + + // net:100%, cpu:100% + { + rentbw_tester t; + init(t, true); + t.transfer(config::system_account_name, N(aaaaaaaaaaaa), core_sym::from_string("3000000.0000")); + BOOST_REQUIRE_EQUAL( + t.wasm_assert_msg("calculated fee is below minimum; try renting more"), + t.rentbw(N(aaaaaaaaaaaa), N(bbbbbbbbbbbb), 30, 10, 10, asset::from_string("3000000.0000 TST"))); + t.check_rentbw(N(aaaaaaaaaaaa), N(bbbbbbbbbbbb), 30, rentbw_frac, rentbw_frac, + asset::from_string("3000000.0000 TST"), net_weight, cpu_weight); + + BOOST_REQUIRE_EQUAL( // + t.wasm_assert_msg("weight can't shrink below utilization"), + t.configbw(t.make_default_config([&](auto& config) { + config.net.current_weight_ratio = rentbw_frac / 4 + 1; + config.net.target_weight_ratio = rentbw_frac / 4 + 1; + config.cpu.current_weight_ratio = rentbw_frac / 5; + config.cpu.target_weight_ratio = rentbw_frac / 5; + }))); + BOOST_REQUIRE_EQUAL( // + t.wasm_assert_msg("weight can't shrink below utilization"), + t.configbw(t.make_default_config([&](auto& config) { + config.net.current_weight_ratio = rentbw_frac / 4; + config.net.target_weight_ratio = rentbw_frac / 4; + config.cpu.current_weight_ratio = rentbw_frac / 5 + 1; + config.cpu.target_weight_ratio = rentbw_frac / 5 + 1; + }))); + BOOST_REQUIRE_EQUAL( // + "", // + t.configbw(t.make_default_config([&](auto& config) { + config.net.current_weight_ratio = rentbw_frac / 4; + config.net.target_weight_ratio = rentbw_frac / 4; + config.cpu.current_weight_ratio = rentbw_frac / 5; + config.cpu.target_weight_ratio = rentbw_frac / 5; + }))); + } + + // net:30%, cpu:40%, then net:5%, cpu:10% + { + rentbw_tester t; + init(t, true); + // (.3 ^ 2) * 2000000.0000 / 2 = 90000.0000 + // (.4 ^ 3) * 6000000.0000 / 3 = 128000.0000 + // total = 218000.0000 + t.transfer(config::system_account_name, N(aaaaaaaaaaaa), core_sym::from_string("218000.0001")); + t.check_rentbw(N(aaaaaaaaaaaa), N(bbbbbbbbbbbb), 30, rentbw_frac * .3, rentbw_frac * .4, + asset::from_string("218000.0001 TST"), net_weight * .3, cpu_weight * .4); + + // (.35 ^ 2) * 2000000.0000 / 2 - 90000.0000 = 32500.0000 + // (.5 ^ 3) * 6000000.0000 / 3 - 128000.0000 = 122000.0000 + // total = 154500.0000 + t.transfer(config::system_account_name, N(aaaaaaaaaaaa), core_sym::from_string("154500.0000")); + t.check_rentbw(N(aaaaaaaaaaaa), N(bbbbbbbbbbbb), 30, rentbw_frac * .05, rentbw_frac * .10, + asset::from_string("154500.0000 TST"), net_weight * .05, cpu_weight * .10); + } + + // net:50%, cpu:50% (but with non-zero min_price and also an exponent of 2 to simplify the math) + { + rentbw_tester t; + init(t, true); + BOOST_REQUIRE_EQUAL("", t.configbw(t.make_default_config([&](auto& config) { + config.cpu.exponent = 2; + config.net.min_price = asset::from_string("1200000.0000 TST"); + config.net.max_price = asset::from_string("2000000.0000 TST"); + + config.cpu.exponent = 2; + config.cpu.min_price = asset::from_string("4000000.0000 TST"); + config.cpu.max_price = asset::from_string("6000000.0000 TST"); + }))); + + // At 0% utilization for both NET and CPU, the cost (in TST) for renting an infinitesimal amount of resources (dr) is + // 1200000.0000 * dr for NET and 4000000.0000 * dr for CPU. + // At 50% utilization for both NET and CPU, the cost (in TST for renting an infinitesimal amount of resources (dr) is + // 1600000.0000 * dr for NET and 5000000.0000 * dr for CPU. + + // The fee for renting 50% of NET (starting from 0% utilization) is expected to be somewhere between + // 1200000.0000 * 0.5 (= 600000.0000) and 1600000.0000 * 0.5 (= 800000.0000). + // In fact, the cost ends up being 700000.0000. + + // The fee for renting 50% of CPU (starting from 0% utilization) is expected to be somewhere between + // 4000000.0000 * 0.5 (= 2000000.0000) and 5000000.0000 * 0.5 (= 2500000.0000). + // In fact, the cost ends up being 2250000.0000. + + + // 1200000.0000 * .5 + (800000.0000 / 2) * (.5 ^ 2) = 700000.0000 + // 4000000.0000 * .5 + (2000000.0000 / 2) * (.5 ^ 2) = 2250000.0000 + // total = 2950000.0000 + t.transfer(config::system_account_name, N(aaaaaaaaaaaa), core_sym::from_string("2950000.0000")); + t.check_rentbw(N(aaaaaaaaaaaa), N(bbbbbbbbbbbb), 30, rentbw_frac * .5, rentbw_frac * .5, + asset::from_string("2950000.0000 TST"), net_weight * .5, cpu_weight * .5); + } + + { + // net:100%, cpu:100% + rentbw_tester t; + init(t, true); + t.transfer(config::system_account_name, N(aaaaaaaaaaaa), core_sym::from_string("3000000.0000")); + t.check_rentbw(N(aaaaaaaaaaaa), N(bbbbbbbbbbbb), 30, rentbw_frac, rentbw_frac, + asset::from_string("3000000.0000 TST"), net_weight, cpu_weight); + + // No more available for 30 days + BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("market doesn't have enough resources available"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac / 1000, rentbw_frac / 1000, + asset::from_string("1.0000 TST"))); + t.produce_block(fc::days(29)); + BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("market doesn't have enough resources available"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac / 1000, rentbw_frac / 1000, + asset::from_string("1.0000 TST"))); + t.produce_block(fc::days(1) - fc::milliseconds(1500)); + BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("market doesn't have enough resources available"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac / 1000, rentbw_frac / 1000, + asset::from_string("1.0000 TST"))); + t.produce_block(fc::milliseconds(500)); + + // immediate renewal: adjusted_utilization doesn't have time to fall + // + // (1.0 ^ 1) * 2000000.0000 = 2000000.0000 + // (1.0 ^ 2) * 6000000.0000 = 6000000.0000 + // total = 8000000.0000 + t.transfer(config::system_account_name, N(aaaaaaaaaaaa), core_sym::from_string("8000000.0000")); + t.check_rentbw(N(aaaaaaaaaaaa), N(bbbbbbbbbbbb), 30, rentbw_frac, rentbw_frac, + asset::from_string("8000000.0000 TST"), 0, 0); + + // No more available for 30 days + BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("market doesn't have enough resources available"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac / 1000, rentbw_frac / 1000, + asset::from_string("1.0000 TST"))); + t.produce_block(fc::days(29)); + BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("market doesn't have enough resources available"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac / 1000, rentbw_frac / 1000, + asset::from_string("1.0000 TST"))); + t.produce_block(fc::days(1) - fc::milliseconds(1000)); + BOOST_REQUIRE_EQUAL(t.wasm_assert_msg("market doesn't have enough resources available"), // + t.rentbw(N(bob111111111), N(alice1111111), 30, rentbw_frac / 1000, rentbw_frac / 1000, + asset::from_string("1.0000 TST"))); + + // Start decay + t.produce_block(fc::milliseconds(1000)); + BOOST_REQUIRE_EQUAL("", t.rentbwexec(config::system_account_name, 10)); + BOOST_REQUIRE_EQUAL("", t.rentbwexec(config::system_account_name, 10)); + BOOST_REQUIRE(near(t.get_state().net.adjusted_utilization, net_weight, net_weight / 1000)); + BOOST_REQUIRE(near(t.get_state().cpu.adjusted_utilization, cpu_weight, cpu_weight / 1000)); + + // 1 day of decay + t.produce_block(fc::days(1) - fc::milliseconds(500)); + BOOST_REQUIRE_EQUAL("", t.rentbwexec(config::system_account_name, 10)); + BOOST_REQUIRE(near(t.get_state().net.adjusted_utilization, int64_t(net_weight * exp(-1)), + int64_t(net_weight * exp(-1)) / 1000)); + BOOST_REQUIRE(near(t.get_state().cpu.adjusted_utilization, int64_t(cpu_weight * exp(-1)), + int64_t(cpu_weight * exp(-1)) / 1000)); + + // 1 day of decay + t.produce_block(fc::days(1) - fc::milliseconds(500)); + BOOST_REQUIRE_EQUAL("", t.rentbwexec(config::system_account_name, 10)); + BOOST_REQUIRE(near(t.get_state().net.adjusted_utilization, int64_t(net_weight * exp(-2)), + int64_t(net_weight * exp(-2)) / 1000)); + BOOST_REQUIRE(near(t.get_state().cpu.adjusted_utilization, int64_t(cpu_weight * exp(-2)), + int64_t(cpu_weight * exp(-2)) / 1000)); + + // 100% after 2 days of decay + // + // [ ((e^-2) ^ 1)*(e^-2 - 0.0) + ((1.0) ^ 2)/2 - ((e^-2) ^ 2)/2 ] * 2000000.0000 = 1018315.6389 + // [ ((e^-2) ^ 2)*(e^-2 - 0.0) + ((1.0) ^ 3)/3 - ((e^-2) ^ 3)/3 ] * 6000000.0000 = 2009915.0087 + // total = 3028230.6476 + t.transfer(config::system_account_name, N(aaaaaaaaaaaa), core_sym::from_string("3028229.8795")); + t.check_rentbw(N(aaaaaaaaaaaa), N(bbbbbbbbbbbb), 30, rentbw_frac, rentbw_frac, + asset::from_string("3028229.8795 TST"), net_weight, cpu_weight); + } + + { + rentbw_tester t; + init(t, true); + + // 10%, 20% + // (.1 ^ 2) * 2000000.0000 / 2 = 10000.0000 + // (.2 ^ 3) * 6000000.0000 / 3 = 16000.0000 + // total = 26000.0000 + t.transfer(config::system_account_name, N(aaaaaaaaaaaa), core_sym::from_string("26000.0002")); + t.check_rentbw(N(aaaaaaaaaaaa), N(bbbbbbbbbbbb), 30, rentbw_frac * .1, rentbw_frac * .2, + asset::from_string("26000.0002 TST"), net_weight * .1, cpu_weight * .2); + + t.produce_block(fc::days(15) - fc::milliseconds(500)); + + // 20%, 20% + // (.3 ^ 2) * 2000000.0000 / 2 - 10000.0000 = 80000.0000 + // (.4 ^ 3) * 6000000.0000 / 3 - 16000.0000 = 112000.0000 + // total = 192000.0000 + t.transfer(config::system_account_name, N(aaaaaaaaaaaa), core_sym::from_string("192000.0001")); + t.check_rentbw(N(aaaaaaaaaaaa), N(bbbbbbbbbbbb), 30, rentbw_frac * .2, rentbw_frac * .2, + asset::from_string("192000.0001 TST"), net_weight * .2, cpu_weight * .2); + + // Start decay + t.produce_block(fc::days(15) - fc::milliseconds(1000)); + BOOST_REQUIRE_EQUAL("", t.rentbwexec(config::system_account_name, 10)); + BOOST_REQUIRE(near(t.get_state().net.adjusted_utilization, .3 * net_weight, 0)); + BOOST_REQUIRE(near(t.get_state().cpu.adjusted_utilization, .4 * cpu_weight, 0)); + + // 1 day of decay from (30%, 40%) to (20%, 20%) + t.produce_block(fc::days(1) - fc::milliseconds(500)); + BOOST_REQUIRE_EQUAL("", t.rentbwexec(config::system_account_name, 10)); + BOOST_REQUIRE( + near(t.get_state().net.adjusted_utilization, int64_t(.1 * net_weight * exp(-1) + .2 * net_weight), 0)); + BOOST_REQUIRE( + near(t.get_state().cpu.adjusted_utilization, int64_t(.2 * cpu_weight * exp(-1) + .2 * cpu_weight), 0)); + + // 2 days of decay from (30%, 40%) to (20%, 20%) + t.produce_block(fc::days(1) - fc::milliseconds(500)); + BOOST_REQUIRE_EQUAL("", t.rentbwexec(config::system_account_name, 10)); + BOOST_REQUIRE( + near(t.get_state().net.adjusted_utilization, int64_t(.1 * net_weight * exp(-2) + .2 * net_weight), 0)); + BOOST_REQUIRE( + near(t.get_state().cpu.adjusted_utilization, int64_t(.2 * cpu_weight * exp(-2) + .2 * cpu_weight), 0)); + } + +} // rent_tests +FC_LOG_AND_RETHROW() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/tests/main.cpp b/tests/main.cpp index 2c658f31a..e3d617509 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -31,7 +31,12 @@ boost::unit_test::test_suite* init_unit_test_suite(int argc, char* argv[]) { break; } } - if(!is_verbose) fc::logger::get(DEFAULT_LOGGER).set_log_level(fc::log_level::off); + + if(is_verbose) { + fc::logger::get(DEFAULT_LOGGER).set_log_level(fc::log_level::debug); + } else { + fc::logger::get(DEFAULT_LOGGER).set_log_level(fc::log_level::off); + } // Register fc::exception translator boost::unit_test::unit_test_monitor.template register_exception_translator(&translate_fc_exception);