From f9ad1dfd105ade94b64a350f03dc1fa0b3e62b6f Mon Sep 17 00:00:00 2001 From: abitmore Date: Sun, 14 Feb 2016 21:43:55 +0800 Subject: [PATCH 1/2] Implement rate limited free transactions feature #603 --- libraries/chain/account_object.cpp | 26 +++++++ .../chain/committee_member_evaluator.cpp | 12 ++++ libraries/chain/db_balance.cpp | 24 +++++++ libraries/chain/evaluator.cpp | 44 ++++++++++++ libraries/chain/hardfork.d/603.hf | 4 ++ .../include/graphene/chain/account_object.hpp | 44 ++++++++++++ .../include/graphene/chain/evaluator.hpp | 29 +++++++- .../chain/protocol/chain_parameters.hpp | 67 ++++++++++++++++++- .../include/graphene/chain/protocol/types.hpp | 13 ++++ 9 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 libraries/chain/hardfork.d/603.hf diff --git a/libraries/chain/account_object.cpp b/libraries/chain/account_object.cpp index 90d97692a5..453c04d706 100644 --- a/libraries/chain/account_object.cpp +++ b/libraries/chain/account_object.cpp @@ -110,6 +110,32 @@ void account_statistics_object::pay_fee( share_type core_fee, share_type cashbac pending_vested_fees += core_fee; } +fc::uint128_t account_statistics_object::compute_coin_seconds_earned(const asset& balance, fc::time_point_sec now)const +{ + if( now <= coin_seconds_earned_last_update ) + return coin_seconds_earned; + int64_t delta_seconds = (now - coin_seconds_earned_last_update).to_seconds(); + + fc::uint128_t delta_coin_seconds = balance.amount.value; + delta_coin_seconds *= delta_seconds; + + return (coin_seconds_earned + delta_coin_seconds); +} + +void account_statistics_object::update_coin_seconds_earned(const asset& balance, fc::time_point_sec now) +{ + if( now <= coin_seconds_earned_last_update ) + return; + coin_seconds_earned = compute_coin_seconds_earned(balance, now); + coin_seconds_earned_last_update = now; +} + +void account_statistics_object::set_coin_seconds_earned(const fc::uint128_t new_coin_seconds, fc::time_point_sec now) +{ + coin_seconds_earned = new_coin_seconds; + coin_seconds_earned_last_update = now; +} + set account_member_index::get_account_members(const account_object& a)const { set result; diff --git a/libraries/chain/committee_member_evaluator.cpp b/libraries/chain/committee_member_evaluator.cpp index 4e7eb827e5..337a77b41b 100644 --- a/libraries/chain/committee_member_evaluator.cpp +++ b/libraries/chain/committee_member_evaluator.cpp @@ -77,6 +77,18 @@ void_result committee_member_update_global_parameters_evaluator::do_evaluate(con { try { FC_ASSERT(trx_state->_is_proposed_trx); + database& d = db(); + + // #603 hard fork check + if( d.head_block_time() <= HARDFORK_603_TIME ) + { + for( const auto& e : o.new_parameters.extensions ) + { + FC_ASSERT( e.which() != chain_parameters::parameter_extension::tag::value, + "New parameters contain an extension which requires hardfork #603." ); + } + } + return void_result(); } FC_CAPTURE_AND_RETHROW( (o) ) } diff --git a/libraries/chain/db_balance.cpp b/libraries/chain/db_balance.cpp index a70f077bb6..dc57e35c5a 100644 --- a/libraries/chain/db_balance.cpp +++ b/libraries/chain/db_balance.cpp @@ -68,12 +68,36 @@ void database::adjust_balance(account_id_type account, asset delta ) b.asset_type = delta.asset_id; b.balance = delta.amount.value; }); + // Update coin_seconds_earned and etc + if( head_block_time() > HARDFORK_603_TIME ) + { + // TODO how to deal with smart coins? + if( delta.asset_id == asset_id_type() ) + { + modify( account(*this).statistics(*this), [&](account_statistics_object& s) { + s.set_coin_seconds_earned( 0, head_block_time() ); + }); + } + } } else { if( delta.amount < 0 ) FC_ASSERT( itr->get_balance() >= -delta, "Insufficient Balance: ${a}'s balance of ${b} is less than required ${r}", ("a",account(*this).name)("b",to_pretty_string(itr->get_balance()))("r",to_pretty_string(-delta))); + const asset original_balance = itr->get_balance(); modify(*itr, [delta](account_balance_object& b) { b.adjust_balance(delta); }); + // Update coin_seconds_earned and etc + if( head_block_time() > HARDFORK_603_TIME ) + { + // TODO how to deal with smart coins? + if( delta.asset_id == asset_id_type() ) + { + modify( account(*this).statistics(*this), [&](account_statistics_object& s) { + //TODO will coin_seconds_earned expire? seems hard to calculate + s.update_coin_seconds_earned( original_balance, head_block_time() ); + }); + } + } } } FC_CAPTURE_AND_RETHROW( (account)(delta) ) } diff --git a/libraries/chain/evaluator.cpp b/libraries/chain/evaluator.cpp index a4127c25b6..59a0357d4e 100644 --- a/libraries/chain/evaluator.cpp +++ b/libraries/chain/evaluator.cpp @@ -79,6 +79,35 @@ database& generic_evaluator::db()const { return trx_state->db(); } } } + void generic_evaluator::prepare_fee_from_coin_seconds(const operation& o) + { + const auto& fee_options = db().get_global_properties().parameters.get_coin_seconds_as_fees_options(); + const auto& max_op_fee = fee_options.max_fee_from_coin_seconds_by_operation; + if( max_op_fee.size() > o.which() && max_op_fee[o.which()] > 0 ) // if fee can be paid with coin seconds + { + const asset& core_balance = db().get_balance( fee_paying_account->get_id(), asset_id_type() ); + const auto payer_membership = fee_paying_account->get_membership( db().head_block_time() ); + coin_seconds_earned = fee_paying_account_statistics->compute_coin_seconds_earned( + core_balance, db().head_block_time() ); + if( coin_seconds_earned > 0 ) // if payer have some coin seconds to pay + { + coin_seconds_as_fees_rate = fee_options.coin_seconds_as_fees_rate[payer_membership]; + fc::uint128_t coin_seconds_to_fees = coin_seconds_earned; + coin_seconds_to_fees /= coin_seconds_as_fees_rate.value; + fees_accumulated_from_coin_seconds = coin_seconds_to_fees.to_uint64(); + + share_type max_fees_allowed = fee_options.max_accumulated_fees_from_coin_seconds[payer_membership]; + if( fees_accumulated_from_coin_seconds > max_fees_allowed ) // if accumulated too many coin seconds, truncate + { + fees_accumulated_from_coin_seconds = max_fees_allowed; + coin_seconds_earned = fc::uint128_t( max_fees_allowed.value ); + coin_seconds_earned *= coin_seconds_as_fees_rate.value; + } + max_fees_payable_with_coin_seconds = std::min( fees_accumulated_from_coin_seconds, max_op_fee[o.which()] ); + } + } + } + void generic_evaluator::convert_fee() { if( !trx_state->skip_fee ) { @@ -128,4 +157,19 @@ database& generic_evaluator::db()const { return trx_state->db(); } db().adjust_balance(fee_payer, fee_from_account); } + void generic_evaluator::pay_fee_with_coin_seconds() + { try { + if( !trx_state->skip_fee ) { + database& d = db(); + // deduct fees from coin_seconds_earned + if( fees_paid_with_coin_seconds > 0 ) + { + fc::uint128_t coin_seconds_consumed( fees_paid_with_coin_seconds.value ); + coin_seconds_consumed *= coin_seconds_as_fees_rate.value; + d.modify(*fee_paying_account_statistics, [&](account_statistics_object& o) { + o.set_coin_seconds_earned( coin_seconds_earned - coin_seconds_consumed, d.head_block_time() ); + }); + } + } + } FC_CAPTURE_AND_RETHROW() } } } diff --git a/libraries/chain/hardfork.d/603.hf b/libraries/chain/hardfork.d/603.hf new file mode 100644 index 0000000000..7a457949b8 --- /dev/null +++ b/libraries/chain/hardfork.d/603.hf @@ -0,0 +1,4 @@ +// #603 simple rate limited free transaction +#ifndef HARDFORK_603_TIME +#define HARDFORK_603_TIME (fc::time_point_sec( 1450378800 )) +#endif diff --git a/libraries/chain/include/graphene/chain/account_object.hpp b/libraries/chain/include/graphene/chain/account_object.hpp index 789dac2ddd..f95682314a 100644 --- a/libraries/chain/include/graphene/chain/account_object.hpp +++ b/libraries/chain/include/graphene/chain/account_object.hpp @@ -22,6 +22,7 @@ * THE SOFTWARE. */ #pragma once +#include #include #include #include @@ -78,6 +79,19 @@ namespace graphene { namespace chain { */ share_type pending_vested_fees; + /** + * Tracks the coin-seconds earned by this account. Lazy updating. + * actual_coin_seconds_earned = coin_seconds_earned + current_balance * (now - coin_seconds_earned_last_update) + */ + // TODO maybe better to use a struct to store coin_seconds_earned and coin_seconds_earned_last_update + // and related functions e.g. compute_coin_seconds_earned and update_coin_seconds_earned + fc::uint128_t coin_seconds_earned; + + /** + * Tracks the most recent time when @ref coin_seconds_earned was updated. + */ + fc::time_point_sec coin_seconds_earned_last_update = HARDFORK_603_TIME; + /// @brief Split up and pay out @ref pending_fees and @ref pending_vested_fees void process_fees(const account_object& a, database& d) const; @@ -85,6 +99,28 @@ namespace graphene { namespace chain { * Core fees are paid into the account_statistics_object by this method */ void pay_fee( share_type core_fee, share_type cashback_vesting_threshold ); + + /** + * Compute coin_seconds_earned. Used to + * non-destructively figure out how many coin seconds + * are available. + */ + // TODO use a public funtion to do this job as well as same job in vesting_balance_object + fc::uint128_t compute_coin_seconds_earned(const asset& balance, fc::time_point_sec now)const; + + /** + * Update coin_seconds_earned and + * coin_seconds_earned_last_update fields due to passing of time + */ + // TODO use a public funtion to do this job and same job in vesting_balance_object + void update_coin_seconds_earned(const asset& balance, fc::time_point_sec now); + + /** + * Update coin_seconds_earned and + * coin_seconds_earned_last_update fields with new data + */ + void set_coin_seconds_earned(const fc::uint128_t new_coin_seconds, fc::time_point_sec now); + }; /** @@ -257,6 +293,13 @@ namespace graphene { namespace chain { { return !is_basic_account(now); } + /// @return membership status of the account. + account_membership get_membership(time_point_sec now)const + { + if( is_basic_account( now ) ) return basic_account; + if( is_lifetime_member() ) return lifetime_member; + return annual_member; + } account_id_type get_id()const { return id; } }; @@ -388,5 +431,6 @@ FC_REFLECT_DERIVED( graphene::chain::account_statistics_object, (total_core_in_orders) (lifetime_fees_paid) (pending_fees)(pending_vested_fees) + (coin_seconds_earned)(coin_seconds_earned_last_update) ) diff --git a/libraries/chain/include/graphene/chain/evaluator.hpp b/libraries/chain/include/graphene/chain/evaluator.hpp index af90517eca..9c6d580d76 100644 --- a/libraries/chain/include/graphene/chain/evaluator.hpp +++ b/libraries/chain/include/graphene/chain/evaluator.hpp @@ -25,6 +25,7 @@ #include #include #include +#include namespace graphene { namespace chain { @@ -95,6 +96,16 @@ namespace graphene { namespace chain { */ void convert_fee(); + /** + * Compute how much can be paid with coin-seconds, need to call after prepare_fee(). + */ + void prepare_fee_from_coin_seconds(const operation& o); + + /** + * Pay fee with coin-seconds + */ + void pay_fee_with_coin_seconds(); + object_id_type get_relative_id( object_id_type rel_id )const; /** @@ -115,6 +126,13 @@ namespace graphene { namespace chain { const asset_object* fee_asset = nullptr; const asset_dynamic_data_object* fee_asset_dyn_data = nullptr; transaction_evaluation_state* trx_state; + // fields for computing fees paid with coin seconds + share_type max_fees_payable_with_coin_seconds = 0; + share_type fees_accumulated_from_coin_seconds = 0; + share_type fees_paid_with_coin_seconds = 0; + share_type coin_seconds_as_fees_rate = 0; + fc::uint128_t coin_seconds_earned = 0; + }; class op_evaluator @@ -147,13 +165,21 @@ namespace graphene { namespace chain { const auto& op = o.get(); prepare_fee(op.fee_payer(), op.fee); + + if( db().head_block_time() > HARDFORK_603_TIME ) + prepare_fee_from_coin_seconds(o); + if( !trx_state->skip_fee_schedule_check ) { share_type required_fee = calculate_fee_for_operation(op); - GRAPHENE_ASSERT( core_fee_paid >= required_fee, + GRAPHENE_ASSERT( core_fee_paid + max_fees_payable_with_coin_seconds >= required_fee, insufficient_fee, "Insufficient Fee Paid", + ("payable_from_coin_seconds", max_fees_payable_with_coin_seconds) ("core_fee_paid",core_fee_paid)("required", required_fee) ); + // if some fees are paid with coin seconds + if( core_fee_paid < required_fee ) + fees_paid_with_coin_seconds = required_fee - core_fee_paid; } return eval->do_evaluate(op); @@ -166,6 +192,7 @@ namespace graphene { namespace chain { convert_fee(); pay_fee(); + pay_fee_with_coin_seconds(); auto result = eval->do_apply(op); diff --git a/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp b/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp index 4dbd6c15ff..68abc018af 100644 --- a/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp +++ b/libraries/chain/include/graphene/chain/protocol/chain_parameters.hpp @@ -36,7 +36,6 @@ namespace fc { namespace graphene { namespace chain { - typedef static_variant<> parameter_extension; struct chain_parameters { /** using a smart ref breaks the circular dependency created between operations and the fee schedule */ @@ -69,14 +68,80 @@ namespace graphene { namespace chain { uint16_t accounts_per_fee_scale = GRAPHENE_DEFAULT_ACCOUNTS_PER_FEE_SCALE; ///< number of accounts between fee scalings uint8_t account_fee_scale_bitshifts = GRAPHENE_DEFAULT_ACCOUNT_FEE_SCALE_BITSHIFTS; ///< number of times to left bitshift account registration fee at each scaling uint8_t max_authority_depth = GRAPHENE_MAX_SIG_CHECK_DEPTH; + + /** + * struct for override extensions + */ + struct ext + { + /** container of coin_seconds to fees parameters */ + struct coin_seconds_as_fees_options + { + /** + * Rates of converting coin_seconds to core asset to pay fees, + * indexed by membership type: [basic_account, lifetime_member, annual_member] + */ + // + // Theoretically, 100 tps means 8.64M transactions a day. + // Total quantity of core asset is 3.7BB, so 1 transaction = 3.7BB/8.64M ~= 428 core assets, + // which means with 428 core assets you can do 1 transaction a day. + // If we set conversion rate to 5000, and set transaction fee to 1, then about 1/10 of network + // capacity can be used freely. A whale with 10M core assets can accumulate same value as + // 1 core asset of coin seconds in 43.2 seconds. An average user with 10K core assets can + // accumulate same value as 1 core asset of coin seconds in 43200 seconds = half a day. + // + // TODO consider nonlinear conversion? + std::vector coin_seconds_as_fees_rate { 86400*20000, + 86400*5000, + 86400*10000 + }; + /** + * Maximum values of accumulated fees which can be paid with accumulated coin_seconds, + * indexed by membership type: [basic_account, lifetime_member, annual_member] + */ + std::vector max_accumulated_fees_from_coin_seconds { GRAPHENE_BLOCKCHAIN_PRECISION * 10, + GRAPHENE_BLOCKCHAIN_PRECISION * 40, + GRAPHENE_BLOCKCHAIN_PRECISION * 20 + }; + /** + * Maximum values of fees can be paid with coin_seconds, indexed by operation_id. + * Default value is all 0. + */ + std::vector max_fee_from_coin_seconds_by_operation; + }; + }; + + typedef static_variant parameter_extension; + typedef flat_set extensions_type; extensions_type extensions; /** defined in fee_schedule.cpp */ void validate()const; + + /** @return the coin_seconds_as_fee_options object set in extensions, or default values if not set */ + const ext::coin_seconds_as_fees_options get_coin_seconds_as_fees_options()const + { + if( extensions.size() > 0 ) + { + for( const parameter_extension& e : extensions ) + { + if( e.which() == parameter_extension::tag::value ) + return e.get(); + } + } + return ext::coin_seconds_as_fees_options(); + } }; } } // graphene::chain +FC_REFLECT( graphene::chain::chain_parameters::ext::coin_seconds_as_fees_options, + (coin_seconds_as_fees_rate) + (max_accumulated_fees_from_coin_seconds) + (max_fee_from_coin_seconds_by_operation) + ) +FC_REFLECT_TYPENAME( graphene::chain::chain_parameters::parameter_extension ) +FC_REFLECT_TYPENAME( graphene::chain::chain_parameters::extensions_type ) FC_REFLECT( graphene::chain::chain_parameters, (current_fees) (block_interval) diff --git a/libraries/chain/include/graphene/chain/protocol/types.hpp b/libraries/chain/include/graphene/chain/protocol/types.hpp index 5237fcad53..2576a94441 100644 --- a/libraries/chain/include/graphene/chain/protocol/types.hpp +++ b/libraries/chain/include/graphene/chain/protocol/types.hpp @@ -100,6 +100,13 @@ namespace graphene { namespace chain { |witness_fed_asset|committee_fed_asset; const static uint32_t UIA_ASSET_ISSUER_PERMISSION_MASK = charge_market_fee|white_list|override_authority|transfer_restricted|disable_confidential; + enum account_membership + { + basic_account = 0, + lifetime_member = 1, + annual_member = 2 + }; + enum reserved_spaces { relative_protocol_ids = 0, @@ -404,3 +411,9 @@ FC_REFLECT_ENUM( graphene::chain::asset_issuer_permission_flags, (witness_fed_asset) (committee_fed_asset) ) + +FC_REFLECT_ENUM( graphene::chain::account_membership, + (basic_account) + (lifetime_member) + (annual_member) + ) From 3edac8c2fa1a729c02b61daba3f878fb7d2986eb Mon Sep 17 00:00:00 2001 From: abitmore Date: Tue, 8 Mar 2016 08:35:10 +0800 Subject: [PATCH 2/2] Unit test for #603 rate limited free transaction --- tests/tests/fee_tests.cpp | 540 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 540 insertions(+) diff --git a/tests/tests/fee_tests.cpp b/tests/tests/fee_tests.cpp index 984c6fcec7..b2ba339dd4 100644 --- a/tests/tests/fee_tests.cpp +++ b/tests/tests/fee_tests.cpp @@ -942,4 +942,544 @@ BOOST_AUTO_TEST_CASE( stealth_fba_test ) } } +/////////////////////////////////// +// coin_seconds_as_fee_test +/////////////////////////////////// +struct actor_coin_seconds_audit +{ + uint64_t b0 = 0; // starting balance parameter + uint64_t bal = 0; // balance should be this + uint64_t csrate = 1; // rate of coin seconds -> fees + fc::uint128_t cs = 0; // coin seconds earned + fc::uint128_t ccs = 0; // cap of coins seconds as fees + fc::uint128_t ecs = 0; // effective coin seconds earned (after capped) + uint64_t ecsfee = 0; // effective fees can be paid by coin seconds +}; + +#define UPDATE_ECS_AUDITOR( actor_name ) \ + if( actor_name ## _id != account_id_type() ) { \ + a_ ## actor_name.ecs = std::min( a_ ## actor_name.cs, a_ ## actor_name.ccs ); \ + fc::uint128_t ecsfee ( a_ ## actor_name.ecs ); \ + ecsfee /= a_ ## actor_name.csrate; \ + a_ ## actor_name.ecsfee = ecsfee.to_uint64(); \ + } + +#define UPDATE_CS_AUDITOR( actor_name, seconds ) \ + if( actor_name ## _id != account_id_type() ) { \ + fc::uint128_t cs_new ( a_ ## actor_name.bal ); \ + cs_new *= seconds; \ + a_ ## actor_name.cs += cs_new; \ + UPDATE_ECS_AUDITOR( actor_name ); \ + } + +#define CHECK_COIN_SECONDS_EARNED( actor_name, amount ) \ + BOOST_CHECK( actor_name ## _id(db).statistics(db).compute_coin_seconds_earned( \ + db.get_balance( actor_name ## _id, asset_id_type() ), db.head_block_time() ) \ + == amount ) + +#define CoinSecondsAuditActor(actor_name) \ + if( actor_name ## _id != account_id_type() ) \ + { \ + CHECK_BALANCE( actor_name, a_ ## actor_name.bal ); \ + CHECK_COIN_SECONDS_EARNED( actor_name, a_ ## actor_name.cs ); \ + } + +#define ENABLE_FEES_FOR_COIN_SECONDS_TEST \ + { \ + enable_fees(); \ + flat_set< fee_parameters > new_fees; \ + { \ + transfer_operation::fee_parameters_type transfer_fee_params; \ + transfer_fee_params.fee = transfer_fee; \ + new_fees.insert( transfer_fee_params ); \ + } \ + change_fees( new_fees ); \ + } + +// update coin_seconds_as_fee options +#define UPDATE_GLOBAL_CS_FEE_OPTIONS \ + {\ + graphene::chain::chain_parameters::ext::coin_seconds_as_fees_options cs_fee_options;\ + /* fee rate: { basic_account, lifetime_member, annual_member } */\ + cs_fee_options.coin_seconds_as_fees_rate = { cs_fee_rate_basic_account,\ + cs_fee_rate_lifetime_member,\ + cs_fee_rate_annual_member };\ + /* fee cap: { basic_account, lifetime_member, annual_member } */\ + cs_fee_options.max_accumulated_fees_from_coin_seconds = { max_cs_fee_basic_account,\ + max_cs_fee_lifetime_member,\ + max_cs_fee_annual_member };\ + /* transfer fee cap */\ + cs_fee_options.max_fee_from_coin_seconds_by_operation = { max_cs_as_transfer_fee };\ + /* apply changes */\ + db.modify(global_property_id_type()(db), [&](global_property_object& gpo)\ + {\ + gpo.parameters.block_interval = block_interval;\ + gpo.parameters.maintenance_interval = block_interval;\ + gpo.parameters.maintenance_skip_slots = 0;\ + graphene::chain::chain_parameters::parameter_extension e = cs_fee_options;\ + gpo.parameters.extensions.clear();\ + gpo.parameters.extensions.emplace_hint( gpo.parameters.extensions.end(), e );\ + });\ + ccs_basic_account = max_cs_fee_basic_account;\ + ccs_basic_account *= cs_fee_rate_basic_account;\ + ccs_lifetime_member = max_cs_fee_lifetime_member;\ + ccs_lifetime_member *= cs_fee_rate_lifetime_member;\ + ccs_annual_member = max_cs_fee_annual_member;\ + ccs_annual_member *= cs_fee_rate_annual_member;\ + ccs_basic_account_x2 = ccs_basic_account;\ + ccs_basic_account_x2 *= 2;\ + ccs_lifetime_member_x2 = ccs_lifetime_member;\ + ccs_lifetime_member_x2 *= 2;\ + ccs_annual_member_x2 = ccs_annual_member;\ + ccs_annual_member_x2 *= 2;\ + } + +BOOST_AUTO_TEST_CASE( coin_seconds_as_fee_test ) +{ try { + + ACTORS((mil)(geo)(bot)(you)(umi)(car)); + + actor_coin_seconds_audit a_mil, a_geo, a_bot, a_you, a_umi, a_car; + a_mil.b0 = 0; + a_geo.b0 = 100000; + a_bot.b0 = 100000; + a_you.b0 = 100000; + a_umi.b0 = 0; + a_car.b0 = 0; + + uint64_t transfer_fee = 2000; + uint64_t max_cs_as_transfer_fee = transfer_fee; // initial transfer fee cap is 100% + + uint64_t block_interval = 1; + + uint64_t cs_fee_rate_basic_account = 100 * 11; // 100 Satoshi of CORE * 11 seconds -> 1 Satoshi of CORE of fee + uint64_t cs_fee_rate_lifetime_member = 100 * 3; + uint64_t cs_fee_rate_annual_member = 100 * 7; + + uint64_t max_cs_fee_basic_account = 2000; // set it to be >= transfer_fee + uint64_t max_cs_fee_lifetime_member = 13000; + uint64_t max_cs_fee_annual_member = 5000; + + fc::uint128_t ccs_basic_account; + fc::uint128_t ccs_lifetime_member; + fc::uint128_t ccs_annual_member; + fc::uint128_t ccs_basic_account_x2; + fc::uint128_t ccs_lifetime_member_x2; + fc::uint128_t ccs_annual_member_x2; + + BOOST_TEST_MESSAGE( "Generate block 1" ); + generate_block(); + + BOOST_TEST_MESSAGE( "Update Global fee options" ); + UPDATE_GLOBAL_CS_FEE_OPTIONS; + + asset fee_0 ( 0 ); + asset fee_50 ( transfer_fee / 2 ); + asset fee_100 ( transfer_fee ); + + // upgrade geo to lifetime member, and upgrade bot to annual member which is valid for 9 years + upgrade_to_lifetime_member( geo_id ); + for( int i = 0; i < 9; i++ ) + upgrade_to_annual_member( bot_id ); + + a_mil.csrate = cs_fee_rate_basic_account; + a_geo.csrate = cs_fee_rate_lifetime_member; + a_bot.csrate = cs_fee_rate_annual_member; + a_you.csrate = cs_fee_rate_basic_account; + a_umi.csrate = cs_fee_rate_basic_account; + a_car.csrate = cs_fee_rate_basic_account; + + a_mil.ccs = ccs_basic_account; + a_geo.ccs = ccs_lifetime_member; + a_bot.ccs = ccs_annual_member; + a_you.ccs = ccs_basic_account; + a_umi.ccs = ccs_basic_account; + a_car.ccs = ccs_basic_account; + + // ecs and ecsfee are 0 at the beginning + +#define UPDATE_CS_AUDITORS( seconds ) \ + { \ + UPDATE_CS_AUDITOR( mil, seconds ); \ + UPDATE_CS_AUDITOR( geo, seconds ); \ + UPDATE_CS_AUDITOR( bot, seconds ); \ + UPDATE_CS_AUDITOR( you, seconds ); \ + UPDATE_CS_AUDITOR( umi, seconds ); \ + UPDATE_CS_AUDITOR( car, seconds ); \ + } + +#define CoinSecondsAudit() \ + { \ + CoinSecondsAuditActor( mil ); \ + CoinSecondsAuditActor( geo ); \ + CoinSecondsAuditActor( bot ); \ + CoinSecondsAuditActor( you ); \ + CoinSecondsAuditActor( umi ); \ + CoinSecondsAuditActor( car ); \ + } + + auto transfer_test = [&]( account_id_type from, account_id_type to, asset amount, asset fee, fc::ecc::private_key pk ) + { + transfer_operation transfer_op; + transfer_op.from = from; + transfer_op.to = to; + transfer_op.amount = amount; + transfer_op.fee = fee; + signed_transaction tx; + tx.operations.push_back( transfer_op ); + set_expiration( db, tx ); + sign( tx, pk ); + PUSH_TX( db, tx ); + }; + + // init funds + transfer( account_id_type(), geo_id, asset(a_geo.b0) ); + a_geo.bal += a_geo.b0; + transfer( account_id_type(), bot_id, asset(a_bot.b0) ); + a_bot.bal += a_bot.b0; + transfer( account_id_type(), you_id, asset(a_you.b0) ); + a_you.bal += a_you.b0; + + BOOST_TEST_MESSAGE( "Init finished" ); + + // first check + CoinSecondsAudit(); + + BOOST_TEST_MESSAGE( "Generate block 2" ); + uint32_t skip = database::skip_witness_signature + | database::skip_transaction_signatures + | database::skip_transaction_dupe_check + | database::skip_block_size_check + | database::skip_tapos_check + | database::skip_authority_check + | database::skip_merkle_check + ; + generate_block( skip ); + + // before hard fork time, and at hard fork time all coin_seconds_earned == 0 + BOOST_TEST_MESSAGE( "Generate blocks to hard fork 603 time" ); + for( int i = -5; i <= 0; i++ ) + { + BOOST_TEST_MESSAGE( "round " << (i) << " begin" ); + generate_blocks( HARDFORK_603_TIME + block_interval * i, true, skip ); + CoinSecondsAudit(); + + // enable fees + ENABLE_FEES_FOR_COIN_SECONDS_TEST; + + // no coin seconds can be used as fees + BOOST_TEST_MESSAGE( "testing 0 fee" ); + GRAPHENE_REQUIRE_THROW( transfer_test( geo_id, mil_id, asset(1), fee_0, geo_private_key ), insufficient_fee ); + GRAPHENE_REQUIRE_THROW( transfer_test( bot_id, mil_id, asset(1), fee_0, bot_private_key ), insufficient_fee ); + GRAPHENE_REQUIRE_THROW( transfer_test( you_id, mil_id, asset(1), fee_0, you_private_key ), insufficient_fee ); + BOOST_TEST_MESSAGE( "testing half fee" ); + GRAPHENE_REQUIRE_THROW( transfer_test( geo_id, mil_id, asset(1), fee_50, geo_private_key ), insufficient_fee ); + GRAPHENE_REQUIRE_THROW( transfer_test( bot_id, mil_id, asset(1), fee_50, bot_private_key ), insufficient_fee ); + GRAPHENE_REQUIRE_THROW( transfer_test( you_id, mil_id, asset(1), fee_50, you_private_key ), insufficient_fee ); + CoinSecondsAudit(); + + // normal transfer is ok + BOOST_TEST_MESSAGE( "testing full fee" ); + transfer_test( you_id, mil_id, asset(1), fee_100, you_private_key ); + a_you.bal -= ( 1 + transfer_fee ); + a_mil.bal += 1; + CoinSecondsAudit(); + BOOST_TEST_MESSAGE( "round " << (i) << " end" ); + } + + // the first block after hard fork time + BOOST_TEST_MESSAGE( "Generate a block after hard fork 603 time" ); + { + generate_block( skip ); + UPDATE_CS_AUDITORS( 1 * block_interval ); + CoinSecondsAudit(); + + // check + BOOST_CHECK_EQUAL(a_mil.bal * 1 * block_interval, a_mil.cs.to_uint64()); + BOOST_CHECK_EQUAL(a_geo.bal * 1 * block_interval, a_geo.cs.to_uint64()); + BOOST_CHECK_EQUAL(a_bot.bal * 1 * block_interval, a_bot.cs.to_uint64()); + BOOST_CHECK_EQUAL(a_you.bal * 1 * block_interval, a_you.cs.to_uint64()); + BOOST_CHECK_EQUAL(a_umi.bal * 1 * block_interval, a_umi.cs.to_uint64()); + BOOST_CHECK_EQUAL(a_car.bal * 1 * block_interval, a_car.cs.to_uint64()); + + // enable fees + ENABLE_FEES_FOR_COIN_SECONDS_TEST; + + if( a_geo.ecsfee < transfer_fee ) + { + GRAPHENE_REQUIRE_THROW( transfer_test( geo_id, mil_id, asset(1), fee_0, geo_private_key ), insufficient_fee ); + } + + CoinSecondsAudit(); + } + + // assume that initial funds are same, + // geo, the lifetime member, should be the first one who can pay 50% of fee by cs + // generate blocks until cs of geo reach the limit + BOOST_TEST_MESSAGE( "Preparing first half fee test" ); + while( a_geo.ecsfee < transfer_fee / 2 ) + { + generate_block( skip ); + UPDATE_CS_AUDITORS( 1 * block_interval ); + CoinSecondsAudit(); + } + BOOST_TEST_MESSAGE( "Block #" << db.head_block_num() ); + // geo transfer some assets to umi, pay 50% of fee by cs + // at same time, most likely bot and you don't have enough coin-seconds to pay 50% of fee + BOOST_TEST_MESSAGE( "First half fee test" ); + { + ENABLE_FEES_FOR_COIN_SECONDS_TEST; + + transfer_test( geo_id, umi_id, asset(1), fee_50, geo_private_key ); + a_geo.bal -= (1 + transfer_fee / 2); + a_umi.bal += 1; + + fc::uint128_t cs_change ( transfer_fee - transfer_fee / 2 ); + cs_change *= cs_fee_rate_lifetime_member; + a_geo.cs = std::min(a_geo.cs, a_geo.ecs); + a_geo.cs -= cs_change; + UPDATE_ECS_AUDITOR( geo ); + + // assume umi's balance was 0, no need to update cs here. + + CoinSecondsAudit(); + + if( a_bot.ecsfee < transfer_fee - transfer_fee / 2 ) + { + GRAPHENE_REQUIRE_THROW( transfer_test( bot_id, mil_id, asset(1), fee_50, bot_private_key ), insufficient_fee ); + GRAPHENE_REQUIRE_THROW( transfer_test( you_id, mil_id, asset(1), fee_50, you_private_key ), insufficient_fee ); + } + } + + // after some blocks, bot, the annual member, should be able to pay 100% of fee by cs + // in the meanwhile, umi started accumulating cs + BOOST_TEST_MESSAGE( "Preparing first 0 fee test" ); + while( a_bot.ecsfee < transfer_fee ) + { + generate_block( skip ); + UPDATE_CS_AUDITORS( 1 * block_interval ); + CoinSecondsAudit(); + } + BOOST_TEST_MESSAGE( "Block #" << db.head_block_num() ); + // bot transfer some assets to umi for free (pay all fees by cs) + // at same time, most likely you don't have enough coin-seconds to pay 100% of fee + BOOST_TEST_MESSAGE( "First 0 fee test" ); + { + ENABLE_FEES_FOR_COIN_SECONDS_TEST; + + transfer_test( bot_id, umi_id, asset(2), fee_0, bot_private_key ); + a_bot.bal -= 2; + a_umi.bal += 2; + + fc::uint128_t cs_change ( transfer_fee ); + cs_change *= cs_fee_rate_annual_member; + a_bot.cs = std::min(a_bot.cs, a_bot.ecs); + a_bot.cs -= cs_change; + UPDATE_ECS_AUDITOR( bot ); + + CoinSecondsAudit(); + + if( a_you.ecsfee < transfer_fee ) + { + GRAPHENE_REQUIRE_THROW( transfer_test( you_id, mil_id, asset(2), fee_0, you_private_key ), insufficient_fee ); + } + } + + // after some blocks, you, the basic member, should be able to pay 100% of fee by cs + BOOST_TEST_MESSAGE( "Preparing second 0 fee test" ); + while( a_you.ecsfee < transfer_fee ) + { + generate_block( skip ); + UPDATE_CS_AUDITORS( 1 * block_interval ); + CoinSecondsAudit(); + } + BOOST_TEST_MESSAGE( "Block #" << db.head_block_num() ); + // you transfer some assets to umi for free (pay all fees by cs) + BOOST_TEST_MESSAGE( "Second 0 fee test" ); + { + ENABLE_FEES_FOR_COIN_SECONDS_TEST; + + transfer_test( you_id, umi_id, asset(3), fee_0, you_private_key ); + a_you.bal -= 3; + a_umi.bal += 3; + + fc::uint128_t cs_change ( transfer_fee ); + cs_change *= cs_fee_rate_basic_account; + a_you.cs = std::min(a_you.cs, a_you.ecs); + a_you.cs -= cs_change; + UPDATE_ECS_AUDITOR( you ); + + CoinSecondsAudit(); + } + + // all actors are accumulating cs, over the caps + BOOST_TEST_MESSAGE( "Preparing membership cap test 1" ); + while( a_geo.cs < ccs_lifetime_member_x2 + || a_bot.cs < ccs_annual_member_x2 + || a_you.cs < ccs_basic_account_x2 ) + { + generate_block( skip ); + UPDATE_CS_AUDITORS( 1 * block_interval ); + CoinSecondsAudit(); + } + BOOST_TEST_MESSAGE( "Block #" << db.head_block_num() ); + // but they can't use more than the cap + BOOST_TEST_MESSAGE( "Membership cap test 1" ); + { + ENABLE_FEES_FOR_COIN_SECONDS_TEST; + + // geo transfer to umi, after that, cs = cap - used + transfer_test( geo_id, umi_id, asset(5), fee_0, geo_private_key ); + a_geo.bal -= 5; + a_umi.bal += 5; + + fc::uint128_t cs_change ( transfer_fee ); + cs_change *= cs_fee_rate_lifetime_member; + a_geo.cs = ccs_lifetime_member; + a_geo.cs -= cs_change; // assume that cap >= tranfer_fee + UPDATE_ECS_AUDITOR( geo ); + + // bot transfer to umi, after that, cs = cap - used + transfer_test( bot_id, umi_id, asset(7), fee_0, bot_private_key ); + a_bot.bal -= 7; + a_umi.bal += 7; + + cs_change = transfer_fee; + cs_change *= cs_fee_rate_annual_member; + a_bot.cs = ccs_annual_member; + a_bot.cs -= cs_change; // assume that cap >= tranfer_fee + UPDATE_ECS_AUDITOR( bot ); + + // you transfer to umi, after that, cs = cap - used + transfer_test( you_id, umi_id, asset(11), fee_0, you_private_key ); + a_you.bal -= 11; + a_umi.bal += 11; + + cs_change = transfer_fee; + cs_change *= cs_fee_rate_basic_account; + a_you.cs = ccs_basic_account; + a_you.cs -= cs_change; // assume that cap >= tranfer_fee + UPDATE_ECS_AUDITOR( you ); + + CoinSecondsAudit(); + } + + // all actors are accumulating cs, over the caps + BOOST_TEST_MESSAGE( "Preparing membership cap test 2" ); + while( a_geo.cs < ccs_lifetime_member_x2 + || a_bot.cs < ccs_annual_member_x2 + || a_you.cs < ccs_basic_account_x2 ) + { + generate_block( skip ); + UPDATE_CS_AUDITORS( 1 * block_interval ); + CoinSecondsAudit(); + } + // at a time point, the cap of coin_seconds_as_fee of basic member is set to 3/4 of total transfer fee by the committee + { + max_cs_fee_basic_account = transfer_fee * 3 / 4; + UPDATE_GLOBAL_CS_FEE_OPTIONS; + a_mil.ccs = ccs_basic_account; + a_you.ccs = ccs_basic_account; + a_umi.ccs = ccs_basic_account; + a_car.ccs = ccs_basic_account; + UPDATE_ECS_AUDITOR( mil ); + UPDATE_ECS_AUDITOR( you ); + UPDATE_ECS_AUDITOR( umi ); + UPDATE_ECS_AUDITOR( car ); + } + { + generate_block( skip ); + UPDATE_CS_AUDITORS( 1 * block_interval ); + CoinSecondsAudit(); + } + BOOST_TEST_MESSAGE( "Block #" << db.head_block_num() ); + // then basic members can't transfer for free, but other members can + BOOST_TEST_MESSAGE( "Membership cap test 2" ); + { + ENABLE_FEES_FOR_COIN_SECONDS_TEST; + + GRAPHENE_REQUIRE_THROW( transfer_test( you_id, mil_id, asset(1), fee_0, you_private_key ), insufficient_fee ); + + transfer_test( geo_id, mil_id, asset(1), fee_0, geo_private_key ); + a_geo.bal -= 1; + a_mil.bal += 1; + + fc::uint128_t cs_change ( transfer_fee ); + cs_change *= cs_fee_rate_lifetime_member; + a_geo.cs = std::min(a_geo.cs, a_geo.ecs); + a_geo.cs -= cs_change; + UPDATE_ECS_AUDITOR( geo ); + + transfer_test( bot_id, mil_id, asset(1), fee_0, bot_private_key ); + a_bot.bal -= 1; + a_mil.bal += 1; + + cs_change = transfer_fee; + cs_change *= cs_fee_rate_annual_member; + a_bot.cs = std::min(a_bot.cs, a_bot.ecs); + a_bot.cs -= cs_change; + UPDATE_ECS_AUDITOR( bot ); + } + // basic members can still transfer at 50% off + BOOST_TEST_MESSAGE( "Membership cap test 2+" ); + { + transfer_test( you_id, umi_id, asset(1), fee_50, you_private_key ); + a_you.bal -= (1 + transfer_fee / 2); + a_umi.bal += 1; + + fc::uint128_t cs_change ( transfer_fee - transfer_fee / 2 ); + cs_change *= cs_fee_rate_basic_account; + a_you.cs = std::min(a_you.cs, a_you.ecs); + a_you.cs -= cs_change; + UPDATE_ECS_AUDITOR( you ); + + } + + // all actors are accumulating cs, over the caps + BOOST_TEST_MESSAGE( "Preparing operation cap test" ); + while( a_geo.cs < ccs_lifetime_member_x2 + || a_bot.cs < ccs_annual_member_x2 + || a_you.cs < ccs_basic_account_x2 ) + { + generate_block( skip ); + UPDATE_CS_AUDITORS( 1 * block_interval ); + CoinSecondsAudit(); + } + BOOST_TEST_MESSAGE( "Block #" << db.head_block_num() ); + // at a time point, the cap of coin_seconds_as_fee of transfer is set to 1/4 of total fee by the committee + { + max_cs_as_transfer_fee = transfer_fee / 4; + UPDATE_GLOBAL_CS_FEE_OPTIONS; + } + { + generate_block( skip ); + UPDATE_CS_AUDITORS( 1 * block_interval ); + CoinSecondsAudit(); + } + // all actors can no longer transfer for free + BOOST_TEST_MESSAGE( "Operation cap test" ); + { + ENABLE_FEES_FOR_COIN_SECONDS_TEST; + GRAPHENE_REQUIRE_THROW( transfer_test( geo_id, mil_id, asset(1), fee_0, geo_private_key ), insufficient_fee ); + GRAPHENE_REQUIRE_THROW( transfer_test( bot_id, mil_id, asset(1), fee_0, bot_private_key ), insufficient_fee ); + GRAPHENE_REQUIRE_THROW( transfer_test( you_id, mil_id, asset(1), fee_0, you_private_key ), insufficient_fee ); + } + // nor at 50% off + { + GRAPHENE_REQUIRE_THROW( transfer_test( geo_id, mil_id, asset(1), fee_50, geo_private_key ), insufficient_fee ); + GRAPHENE_REQUIRE_THROW( transfer_test( bot_id, mil_id, asset(1), fee_50, bot_private_key ), insufficient_fee ); + GRAPHENE_REQUIRE_THROW( transfer_test( you_id, mil_id, asset(1), fee_50, you_private_key ), insufficient_fee ); + } + BOOST_TEST_MESSAGE( "The end" ); + + //idump( ( get_operation_history( mil_id ) ) ); + //idump( ( get_operation_history( geo_id ) ) ); + //idump( ( get_operation_history( bot_id ) ) ); + //idump( ( get_operation_history( you_id ) ) ); + //idump( ( get_operation_history( umi_id ) ) ); + //idump( ( get_operation_history( car_id ) ) ); + +} FC_LOG_AND_RETHROW() } + + BOOST_AUTO_TEST_SUITE_END()