diff --git a/libraries/chain/db_maint.cpp b/libraries/chain/db_maint.cpp index b175b8de..74c9dd85 100644 --- a/libraries/chain/db_maint.cpp +++ b/libraries/chain/db_maint.cpp @@ -122,19 +122,21 @@ 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(now - dpo.last_son_payout_time >= fc::days(1)) { + 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](const object& o) { + 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_params.son_pay_daily_max())/total_txs_signed; + 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 ); @@ -471,6 +473,8 @@ void database::initialize_budget_record( fc::time_point_sec now, budget_record& rec.from_accumulated_fees = core_dd.accumulated_fees; rec.from_unused_witness_budget = dpo.witness_budget; + + if( (dpo.last_budget_time == fc::time_point_sec()) || (now <= dpo.last_budget_time) ) { @@ -557,11 +561,11 @@ void database::process_budget() // 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; - rec.leftover_son_funds = dpo.son_budget; - available_funds += rec.leftover_son_funds; available_funds -= son_budget; } diff --git a/libraries/chain/include/graphene/chain/son_object.hpp b/libraries/chain/include/graphene/chain/son_object.hpp index e841cb4b..dc5d3285 100644 --- a/libraries/chain/include/graphene/chain/son_object.hpp +++ b/libraries/chain/include/graphene/chain/son_object.hpp @@ -20,7 +20,7 @@ namespace graphene { namespace chain { static const uint8_t space_id = implementation_ids; static const uint8_t type_id = impl_son_statistics_object_type; - account_id_type owner; + son_id_type owner; // Transactions signed since the last son payouts uint64_t txs_signed = 0; }; diff --git a/tests/common/database_fixture.cpp b/tests/common/database_fixture.cpp index f31ec00a..376b8f82 100644 --- a/tests/common/database_fixture.cpp +++ b/tests/common/database_fixture.cpp @@ -270,6 +270,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()