Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement rate limited free transactions feature #603 #612

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions libraries/chain/account_object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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_id_type> account_member_index::get_account_members(const account_object& a)const
{
set<account_id_type> result;
Expand Down
12 changes: 12 additions & 0 deletions libraries/chain/committee_member_evaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<chain_parameters::ext::coin_seconds_as_fees_options>::value,
"New parameters contain an extension which requires hardfork #603." );
}
}

return void_result();
} FC_CAPTURE_AND_RETHROW( (o) ) }

Expand Down
24 changes: 24 additions & 0 deletions libraries/chain/db_balance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) ) }
Expand Down
44 changes: 44 additions & 0 deletions libraries/chain/evaluator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) {
Expand Down Expand Up @@ -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() }
} }
4 changes: 4 additions & 0 deletions libraries/chain/hardfork.d/603.hf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// #603 simple rate limited free transaction
#ifndef HARDFORK_603_TIME
#define HARDFORK_603_TIME (fc::time_point_sec( 1450378800 ))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

epoch time?
Why is set to 'Thu, 17 Dec 2015 19:00:00 GMT'?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In develop branch it's just a random date. In final release it would be changed according to the plan.

#endif
44 changes: 44 additions & 0 deletions libraries/chain/include/graphene/chain/account_object.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* THE SOFTWARE.
*/
#pragma once
#include <graphene/chain/hardfork.hpp>
#include <graphene/chain/protocol/operations.hpp>
#include <graphene/db/generic_index.hpp>
#include <boost/multi_index/composite_key.hpp>
Expand Down Expand Up @@ -78,13 +79,48 @@ 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;

/**
* 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);

};

/**
Expand Down Expand Up @@ -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; }
};
Expand Down Expand Up @@ -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)
)

29 changes: 28 additions & 1 deletion libraries/chain/include/graphene/chain/evaluator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <graphene/chain/exceptions.hpp>
#include <graphene/chain/transaction_evaluation_state.hpp>
#include <graphene/chain/protocol/operations.hpp>
#include <graphene/chain/hardfork.hpp>

namespace graphene { namespace chain {

Expand Down Expand Up @@ -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;

/**
Expand All @@ -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
Expand Down Expand Up @@ -147,13 +165,21 @@ namespace graphene { namespace chain {
const auto& op = o.get<typename DerivedEvaluator::operation_type>();

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);
Expand All @@ -166,6 +192,7 @@ namespace graphene { namespace chain {

convert_fee();
pay_fee();
pay_fee_with_coin_seconds();

auto result = eval->do_apply(op);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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<share_type> 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<share_type> 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<share_type> max_fee_from_coin_seconds_by_operation;
};
};

typedef static_variant<ext::coin_seconds_as_fees_options> parameter_extension;
typedef flat_set<parameter_extension> 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<ext::coin_seconds_as_fees_options>::value )
return e.get<ext::coin_seconds_as_fees_options>();
}
}
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)
Expand Down
Loading