diff --git a/libraries/chain/CMakeLists.txt b/libraries/chain/CMakeLists.txt index a8969c41..5688eabf 100644 --- a/libraries/chain/CMakeLists.txt +++ b/libraries/chain/CMakeLists.txt @@ -114,6 +114,7 @@ add_library( graphene_chain affiliate_payout.cpp son_evaluator.cpp + son_object.cpp ${HEADERS} ${PROTOCOL_HEADERS} diff --git a/libraries/chain/db_init.cpp b/libraries/chain/db_init.cpp index 02634c06..445e3adc 100644 --- a/libraries/chain/db_init.cpp +++ b/libraries/chain/db_init.cpp @@ -317,6 +317,7 @@ void database::initialize_indexes() add_index< primary_index >(); add_index< primary_index >(); + add_index< primary_index >(); } diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index 765d3c72..6646d2e1 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -117,6 +117,53 @@ void database::update_worker_votes() } } +void database::pay_sons() +{ + time_point_sec now = head_block_time(); + const dynamic_global_property_object& dpo = get_dynamic_global_properties(); + // Current requirement is that we have to pay every 24 hours, so the following check + if( dpo.son_budget.value > 0 && now - dpo.last_son_payout_time >= fc::days(1)) { + uint64_t total_txs_signed = 0; + share_type son_budget = dpo.son_budget; + get_index_type().inspect_all_objects([this, &total_txs_signed](const object& o) { + const son_statistics_object& s = static_cast(o); + total_txs_signed += s.txs_signed; + }); + + + // Now pay off each SON proportional to the number of transactions signed. + get_index_type().inspect_all_objects([this, &total_txs_signed, &dpo, &son_budget](const object& o) { + const son_statistics_object& s = static_cast(o); + if(s.txs_signed > 0){ + auto son_params = get_global_properties().parameters; + share_type pay = (s.txs_signed * son_budget.value)/total_txs_signed; + + const auto& idx = get_index_type().indices().get(); + auto son_obj = idx.find( s.owner ); + modify( *son_obj, [&]( son_object& _son_obj) + { + _son_obj.pay_son_fee(pay, *this); + }); + //Remove the amount paid out to SON from global SON Budget + modify( dpo, [&]( dynamic_global_property_object& _dpo ) + { + _dpo.son_budget -= pay; + } ); + //Reset the tx counter in each son statistics object + modify( s, [&]( son_statistics_object& _s) + { + _s.txs_signed = 0; + }); + } + }); + //Note the last son pay out time + modify( dpo, [&]( dynamic_global_property_object& _dpo ) + { + _dpo.last_son_payout_time = now; + }); + } +} + void database::pay_workers( share_type& budget ) { // ilog("Processing payroll! Available budget is ${b}", ("b", budget)); @@ -505,6 +552,21 @@ void database::process_budget() rec.witness_budget = witness_budget; available_funds -= witness_budget; + // We should not factor-in the son budget before SON HARDFORK + share_type son_budget = 0; + if(now >= HARDFORK_SON_TIME){ + // Before making a budget we should pay out SONs for the last day + // This function should check if its time to pay sons + // and modify the global son funds accordingly, whatever is left is passed on to next budget + pay_sons(); + rec.leftover_son_funds = dpo.son_budget; + available_funds += rec.leftover_son_funds; + son_budget = gpo.parameters.son_pay_daily_max(); + son_budget = std::min(son_budget, available_funds); + rec.son_budget = son_budget; + available_funds -= son_budget; + } + fc::uint128_t worker_budget_u128 = gpo.parameters.worker_budget_per_day.value; worker_budget_u128 *= uint64_t(time_to_maint); worker_budget_u128 /= 60*60*24; @@ -524,9 +586,11 @@ void database::process_budget() rec.supply_delta = rec.witness_budget + rec.worker_budget + + rec.son_budget - rec.leftover_worker_funds - rec.from_accumulated_fees - - rec.from_unused_witness_budget; + - rec.from_unused_witness_budget + - rec.leftover_son_funds; modify(core, [&]( asset_dynamic_data_object& _core ) { @@ -535,9 +599,11 @@ void database::process_budget() assert( rec.supply_delta == witness_budget + worker_budget + + son_budget - leftover_worker_funds - _core.accumulated_fees - dpo.witness_budget + - dpo.son_budget ); _core.accumulated_fees = 0; }); @@ -548,6 +614,7 @@ void database::process_budget() // available_funds, we replace it with witness_budget // instead of adding it. _dpo.witness_budget = witness_budget; + _dpo.son_budget = son_budget; _dpo.last_budget_time = now; }); diff --git a/libraries/chain/include/graphene/chain/budget_record_object.hpp b/libraries/chain/include/graphene/chain/budget_record_object.hpp index 49544793..63784c71 100644 --- a/libraries/chain/include/graphene/chain/budget_record_object.hpp +++ b/libraries/chain/include/graphene/chain/budget_record_object.hpp @@ -46,9 +46,11 @@ struct budget_record // sinks of budget, should sum up to total_budget share_type witness_budget = 0; share_type worker_budget = 0; + share_type son_budget = 0; // unused budget share_type leftover_worker_funds = 0; + share_type leftover_son_funds = 0; // change in supply due to budget operations share_type supply_delta = 0; diff --git a/libraries/chain/include/graphene/chain/config.hpp b/libraries/chain/include/graphene/chain/config.hpp index dd25b6ad..8d0baaf8 100644 --- a/libraries/chain/include/graphene/chain/config.hpp +++ b/libraries/chain/include/graphene/chain/config.hpp @@ -232,8 +232,8 @@ #define TOURNAMENT_MAX_START_TIME_IN_FUTURE (60*60*24*7*4) // 1 month #define TOURNAMENT_MAX_START_DELAY (60*60*24*7) // 1 week #define MIN_SON_MEMBER_COUNT 15 - #define SWEEPS_DEFAULT_DISTRIBUTION_PERCENTAGE (2*GRAPHENE_1_PERCENT) #define SWEEPS_DEFAULT_DISTRIBUTION_ASSET (graphene::chain::asset_id_type(0)) #define SWEEPS_VESTING_BALANCE_MULTIPLIER 100000000 #define SWEEPS_ACCUMULATOR_ACCOUNT (graphene::chain::account_id_type(0)) +#define MIN_SON_PAY_DAILY_MAX (GRAPHENE_BLOCKCHAIN_PRECISION * int64_t(200)) diff --git a/libraries/chain/include/graphene/chain/database.hpp b/libraries/chain/include/graphene/chain/database.hpp index 9fe285b4..5f34eeaa 100644 --- a/libraries/chain/include/graphene/chain/database.hpp +++ b/libraries/chain/include/graphene/chain/database.hpp @@ -516,6 +516,7 @@ namespace graphene { namespace chain { void initialize_budget_record( fc::time_point_sec now, budget_record& rec )const; void process_budget(); void pay_workers( share_type& budget ); + void pay_sons(); void perform_chain_maintenance(const signed_block& next_block, const global_property_object& global_props); void update_active_witnesses(); void update_active_committee_members(); diff --git a/libraries/chain/include/graphene/chain/global_property_object.hpp b/libraries/chain/include/graphene/chain/global_property_object.hpp index 788ccdaf..cb93fcf1 100644 --- a/libraries/chain/include/graphene/chain/global_property_object.hpp +++ b/libraries/chain/include/graphene/chain/global_property_object.hpp @@ -79,6 +79,9 @@ namespace graphene { namespace chain { time_point_sec next_maintenance_time; time_point_sec last_budget_time; share_type witness_budget; + //Last SON Payout time, it can be different to the maintenance interval time + time_point_sec last_son_payout_time; + share_type son_budget = 0; uint32_t accounts_registered_this_interval = 0; /** * Every time a block is missed this increases by diff --git a/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp b/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp index 2cfedb95..4ce5340d 100644 --- a/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp +++ b/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp @@ -41,6 +41,7 @@ namespace graphene { namespace chain { optional< uint16_t > sweeps_distribution_percentage; optional< asset_id_type > sweeps_distribution_asset; optional< account_id_type > sweeps_vesting_accumulator_account; + optional < uint32_t > son_pay_daily_max; }; struct chain_parameters @@ -124,6 +125,9 @@ namespace graphene { namespace chain { inline uint16_t son_count()const { return extensions.value.son_count.valid() ? *extensions.value.son_count : MIN_SON_MEMBER_COUNT; } + inline uint16_t son_pay_daily_max()const { + return extensions.value.son_pay_daily_max.valid() ? *extensions.value.son_pay_daily_max : MIN_SON_PAY_DAILY_MAX; + } }; } } // graphene::chain @@ -138,6 +142,7 @@ FC_REFLECT( graphene::chain::parameter_extension, (sweeps_distribution_percentage) (sweeps_distribution_asset) (sweeps_vesting_accumulator_account) + (son_pay_daily_max) ) FC_REFLECT( graphene::chain::chain_parameters, diff --git a/libraries/chain/include/graphene/chain/protocol/types.hpp b/libraries/chain/include/graphene/chain/protocol/types.hpp index 707f9274..c1e592f8 100644 --- a/libraries/chain/include/graphene/chain/protocol/types.hpp +++ b/libraries/chain/include/graphene/chain/protocol/types.hpp @@ -174,7 +174,8 @@ namespace graphene { namespace chain { impl_betting_market_position_object_type, impl_global_betting_statistics_object_type, impl_lottery_balance_object_type, - impl_sweeps_vesting_balance_object_type + impl_sweeps_vesting_balance_object_type, + impl_son_statistics_object_type }; //typedef fc::unsigned_int object_id_type; @@ -256,6 +257,7 @@ namespace graphene { namespace chain { class global_betting_statistics_object; class lottery_balance_object; class sweeps_vesting_balance_object; + class son_statistics_object; typedef object_id< implementation_ids, impl_global_property_object_type, global_property_object> global_property_id_type; typedef object_id< implementation_ids, impl_dynamic_global_property_object_type, dynamic_global_property_object> dynamic_global_property_id_type; @@ -284,6 +286,7 @@ namespace graphene { namespace chain { typedef object_id< implementation_ids, impl_global_betting_statistics_object_type, global_betting_statistics_object > global_betting_statistics_id_type; typedef object_id< implementation_ids, impl_lottery_balance_object_type, lottery_balance_object > lottery_balance_id_type; typedef object_id< implementation_ids, impl_sweeps_vesting_balance_object_type, sweeps_vesting_balance_object> sweeps_vesting_balance_id_type; + typedef object_id< implementation_ids, impl_son_statistics_object_type, son_statistics_object > son_statistics_id_type; typedef fc::array symbol_type; typedef fc::ripemd160 block_id_type; @@ -441,6 +444,7 @@ FC_REFLECT_ENUM( graphene::chain::impl_object_type, (impl_global_betting_statistics_object_type) (impl_lottery_balance_object_type) (impl_sweeps_vesting_balance_object_type) + (impl_son_statistics_object_type) ) FC_REFLECT_TYPENAME( graphene::chain::share_type ) diff --git a/libraries/chain/include/graphene/chain/son_object.hpp b/libraries/chain/include/graphene/chain/son_object.hpp index ba84fadb..dc5d3285 100644 --- a/libraries/chain/include/graphene/chain/son_object.hpp +++ b/libraries/chain/include/graphene/chain/son_object.hpp @@ -6,6 +6,25 @@ namespace graphene { namespace chain { using namespace graphene::db; + /** + * @class son_statistics_object + * @ingroup object + * @ingroup implementation + * + * This object contains regularly updated statistical data about an SON. It is provided for the purpose of + * separating the SON transaction data that changes frequently from the SON object data that is mostly static. + */ + class son_statistics_object : public graphene::db::abstract_object + { + public: + static const uint8_t space_id = implementation_ids; + static const uint8_t type_id = impl_son_statistics_object_type; + + son_id_type owner; + // Transactions signed since the last son payouts + uint64_t txs_signed = 0; + }; + /** * @class son_object * @brief tracks information about a SON account. @@ -24,6 +43,9 @@ namespace graphene { namespace chain { vesting_balance_id_type deposit; public_key_type signing_key; vesting_balance_id_type pay_vb; + son_statistics_id_type statistics; + + void pay_son_fee(share_type pay, database& db); }; struct by_account; @@ -43,7 +65,22 @@ namespace graphene { namespace chain { > >; using son_index = generic_index; + + using son_stats_multi_index_type = multi_index_container< + son_statistics_object, + indexed_by< + ordered_unique< tag, member< object, object_id_type, &object::id > > + > + >; + + using son_stats_index = generic_index; } } // graphene::chain FC_REFLECT_DERIVED( graphene::chain::son_object, (graphene::db::object), (son_account)(vote_id)(total_votes)(url)(deposit)(signing_key)(pay_vb) ) + +FC_REFLECT_DERIVED( graphene::chain::son_statistics_object, + (graphene::db::object), + (owner) + (txs_signed) + ) diff --git a/libraries/chain/son_evaluator.cpp b/libraries/chain/son_evaluator.cpp index 6d70dc62..e330452f 100644 --- a/libraries/chain/son_evaluator.cpp +++ b/libraries/chain/son_evaluator.cpp @@ -27,6 +27,7 @@ object_id_type create_son_evaluator::do_apply(const son_create_operation& op) obj.deposit = op.deposit; obj.signing_key = op.signing_key; obj.pay_vb = op.pay_vb; + obj.statistics = db().create([&](son_statistics_object& s){s.owner = obj.id;}).id; }); return new_son_object.id; } FC_CAPTURE_AND_RETHROW( (op) ) } diff --git a/libraries/chain/son_object.cpp b/libraries/chain/son_object.cpp new file mode 100644 index 00000000..2d3c48ae --- /dev/null +++ b/libraries/chain/son_object.cpp @@ -0,0 +1,8 @@ +#include +#include + +namespace graphene { namespace chain { + void son_object::pay_son_fee(share_type pay, database& db) { + db.adjust_balance(son_account, pay); + } +}} diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index f61a462b..c9917c95 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -272,6 +272,7 @@ void database_fixture::verify_asset_supplies( const database& db ) total_balances[db.get_global_properties().parameters.sweeps_distribution_asset()] += sweeps_vestings / SWEEPS_VESTING_BALANCE_MULTIPLIER; total_balances[asset_id_type()] += db.get_dynamic_global_properties().witness_budget; + total_balances[asset_id_type()] += db.get_dynamic_global_properties().son_budget; for( const auto& item : total_debts ) { diff --git a/tests/tests/son_operations_tests.cpp b/tests/tests/son_operations_tests.cpp index db992544..4d8d571f 100644 --- a/tests/tests/son_operations_tests.cpp +++ b/tests/tests/son_operations_tests.cpp @@ -182,5 +182,214 @@ catch (fc::exception &e) { edump((e.to_detail_string())); throw; } +} + +BOOST_AUTO_TEST_CASE( son_pay_test ) +{ + try + { + const dynamic_global_property_object& dpo = db.get_dynamic_global_properties(); + const auto block_interval = db.get_global_properties().parameters.block_interval; + BOOST_CHECK( dpo.son_budget.value == 0); + generate_blocks(HARDFORK_SON_TIME); + while (db.head_block_time() <= HARDFORK_SON_TIME) { + generate_block(); + } + generate_block(); + set_expiration(db, trx); + + ACTORS((alice)(bob)); + // Send some core to the actors + transfer( committee_account, alice_id, asset( 20000 * 100000) ); + transfer( committee_account, bob_id, asset( 20000 * 100000) ); + + generate_block(); + // Enable default fee schedule to collect fees + enable_fees(); + // Make SON Budget small for testing purposes + // Make witness budget zero so that amount can be allocated to SON + db.modify( db.get_global_properties(), [&]( global_property_object& _gpo ) + { + _gpo.parameters.extensions.value.son_pay_daily_max = 200; + _gpo.parameters.witness_pay_per_block = 0; + } ); + // Upgrades pay fee and this goes to reserve + upgrade_to_lifetime_member(alice); + upgrade_to_lifetime_member(bob); + // Note payment time just to generate enough blocks to make budget + auto pay_fee_time = db.head_block_time().sec_since_epoch(); + generate_block(); + // Do maintenance from the upcoming block + auto schedule_maint = [&]() + { + db.modify( db.get_dynamic_global_properties(), [&]( dynamic_global_property_object& _dpo ) + { + _dpo.next_maintenance_time = db.head_block_time() + 1; + } ); + }; + + // Generate enough blocks to make budget + while( db.head_block_time().sec_since_epoch() - pay_fee_time < 100 * block_interval ) + { + generate_block(); + } + + // Enough blocks generated schedule maintenance now + schedule_maint(); + // This block triggers maintenance + generate_block(); + + // Check that the SON Budget is allocated and Witness budget is zero + BOOST_CHECK( dpo.son_budget.value == 200); + BOOST_CHECK( dpo.witness_budget.value == 0); + + // Now create SONs + std::string test_url1 = "https://create_son_test1"; + std::string test_url2 = "https://create_son_test2"; + + // create deposit vesting + vesting_balance_id_type deposit1; + { + vesting_balance_create_operation op; + op.creator = alice_id; + op.owner = alice_id; + op.amount = asset(10); + trx.operations.push_back(op); + for( auto& op : trx.operations ) db.current_fee_schedule().set_fee(op); + set_expiration(db, trx); + processed_transaction ptx = PUSH_TX(db, trx, ~0); + trx.clear(); + deposit1 = ptx.operation_results[0].get(); + } + + // create payment vesting + vesting_balance_id_type payment1; + { + vesting_balance_create_operation op; + op.creator = alice_id; + op.owner = alice_id; + op.amount = asset(10); + trx.operations.push_back(op); + for( auto& op : trx.operations ) db.current_fee_schedule().set_fee(op); + set_expiration(db, trx); + processed_transaction ptx = PUSH_TX(db, trx, ~0); + trx.clear(); + payment1 = ptx.operation_results[0].get(); + } + + // create deposit vesting + vesting_balance_id_type deposit2; + { + vesting_balance_create_operation op; + op.creator = bob_id; + op.owner = bob_id; + op.amount = asset(10); + trx.operations.push_back(op); + for( auto& op : trx.operations ) db.current_fee_schedule().set_fee(op); + set_expiration(db, trx); + processed_transaction ptx = PUSH_TX(db, trx, ~0); + trx.clear(); + deposit2 = ptx.operation_results[0].get(); + } + + // create payment vesting + vesting_balance_id_type payment2; + { + vesting_balance_create_operation op; + op.creator = bob_id; + op.owner = bob_id; + op.amount = asset(10); + trx.operations.push_back(op); + for( auto& op : trx.operations ) db.current_fee_schedule().set_fee(op); + set_expiration(db, trx); + processed_transaction ptx = PUSH_TX(db, trx, ~0); + trx.clear(); + payment2 = ptx.operation_results[0].get(); + } + + // alice becomes son + { + son_create_operation op; + op.owner_account = alice_id; + op.url = test_url1; + op.deposit = deposit1; + op.pay_vb = payment1; + op.fee = asset(0); + op.signing_key = alice_public_key; + trx.operations.push_back(op); + for( auto& op : trx.operations ) db.current_fee_schedule().set_fee(op); + sign(trx, alice_private_key); + PUSH_TX(db, trx, ~0); + trx.clear(); + } + + // bob becomes son + { + son_create_operation op; + op.owner_account = bob_id; + op.url = test_url2; + op.deposit = deposit2; + op.pay_vb = payment2; + op.fee = asset(0); + op.signing_key = bob_public_key; + trx.operations.push_back(op); + for( auto& op : trx.operations ) db.current_fee_schedule().set_fee(op); + sign(trx, bob_private_key); + PUSH_TX(db, trx, ~0); + trx.clear(); + } + + generate_block(); + // Check if SONs are created properly + const auto& idx = db.get_index_type().indices().get(); + BOOST_REQUIRE( idx.size() == 2 ); + // Alice's SON + auto obj1 = idx.find( alice_id ); + BOOST_REQUIRE( obj1 != idx.end() ); + BOOST_CHECK( obj1->url == test_url1 ); + BOOST_CHECK( obj1->signing_key == alice_public_key ); + BOOST_CHECK( obj1->deposit.instance == deposit1.instance.value ); + BOOST_CHECK( obj1->pay_vb.instance == payment1.instance.value ); + // Bob's SON + auto obj2 = idx.find( bob_id ); + BOOST_REQUIRE( obj2 != idx.end() ); + BOOST_CHECK( obj2->url == test_url2 ); + BOOST_CHECK( obj2->signing_key == bob_public_key ); + BOOST_CHECK( obj2->deposit.instance == deposit2.instance.value ); + BOOST_CHECK( obj2->pay_vb.instance == payment2.instance.value ); + // Get the statistics object for the SONs + const auto& sidx = db.get_index_type().indices().get(); + BOOST_REQUIRE( sidx.size() == 2 ); + auto son_stats_obj1 = sidx.find( obj1->statistics ); + auto son_stats_obj2 = sidx.find( obj2->statistics ); + BOOST_REQUIRE( son_stats_obj1 != sidx.end() ); + BOOST_REQUIRE( son_stats_obj2 != sidx.end() ); + // Modify the transaction signed statistics of Alice's SON + db.modify( *son_stats_obj1, [&]( son_statistics_object& _s) + { + _s.txs_signed = 2; + }); + // Modify the transaction signed statistics of Bob's SON + db.modify( *son_stats_obj2, [&]( son_statistics_object& _s) + { + _s.txs_signed = 3; + }); + + // Note the balances before the maintenance + int64_t obj1_balance = db.get_balance(obj1->son_account, asset_id_type()).amount.value; + int64_t obj2_balance = db.get_balance(obj2->son_account, asset_id_type()).amount.value; + // Next maintenance triggerred + generate_blocks(dpo.next_maintenance_time); + generate_block(); + // Check if the signed transaction statistics are reset for both SONs + BOOST_REQUIRE_EQUAL(son_stats_obj1->txs_signed, 0); + BOOST_REQUIRE_EQUAL(son_stats_obj2->txs_signed, 0); + // Check that Alice and Bob are paid for signing the transactions in the previous day/cycle + BOOST_REQUIRE_EQUAL(db.get_balance(obj1->son_account, asset_id_type()).amount.value, 80+obj1_balance); + BOOST_REQUIRE_EQUAL(db.get_balance(obj2->son_account, asset_id_type()).amount.value, 120+obj2_balance); + // Check the SON Budget is again allocated after maintenance + BOOST_CHECK( dpo.son_budget.value == 200); + BOOST_CHECK( dpo.witness_budget.value == 0); + }FC_LOG_AND_RETHROW() } BOOST_AUTO_TEST_SUITE_END()