Skip to content

Commit

Permalink
Implement rate limited free transactions feature cryptonomex#603
Browse files Browse the repository at this point in the history
  • Loading branch information
abitmore committed Mar 3, 2016
1 parent 529f0be commit f9ad1df
Show file tree
Hide file tree
Showing 9 changed files with 261 additions and 2 deletions.
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 ))
#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

0 comments on commit f9ad1df

Please sign in to comment.