Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Commit

Permalink
Consolidated Security Fixes for 2.0.10
Browse files Browse the repository at this point in the history
- Fix issue with account query db that could result in incorrect data or hung processes
- Implement a Subjective CPU billing system that helps P2P and API nodes better respond to extreme network congestion

Co-Authored-By: Bart Wyatt [email protected]
Co-Authored-By: Kevin Heifner [email protected]
  • Loading branch information
johndebord committed Feb 25, 2021
1 parent 6cdfb9f commit 9c254a9
Show file tree
Hide file tree
Showing 16 changed files with 682 additions and 132 deletions.
14 changes: 9 additions & 5 deletions libraries/chain/controller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1403,7 +1403,8 @@ struct controller_impl {
transaction_trace_ptr push_transaction( const transaction_metadata_ptr& trx,
fc::time_point deadline,
uint32_t billed_cpu_time_us,
bool explicit_billed_cpu_time )
bool explicit_billed_cpu_time,
uint32_t subjective_cpu_bill_us )
{
EOS_ASSERT(deadline != fc::time_point(), transaction_exception, "deadline cannot be uninitialized");

Expand Down Expand Up @@ -1432,6 +1433,7 @@ struct controller_impl {
trx_context.deadline = deadline;
trx_context.explicit_billed_cpu_time = explicit_billed_cpu_time;
trx_context.billed_cpu_time_us = billed_cpu_time_us;
trx_context.subjective_cpu_bill_us = subjective_cpu_bill_us;
trace = trx_context.trace;
try {
if( trx->implicit ) {
Expand Down Expand Up @@ -1504,6 +1506,7 @@ struct controller_impl {
trace->error_code = controller::convert_exception_to_error_code( e );
trace->except = e;
trace->except_ptr = std::current_exception();
trace->elapsed = fc::time_point::now() - trx_context.start;
}

emit( self.accepted_transaction, trx );
Expand Down Expand Up @@ -1645,7 +1648,7 @@ struct controller_impl {
in_trx_requiring_checks = old_value;
});
in_trx_requiring_checks = true;
push_transaction( onbtrx, fc::time_point::maximum(), self.get_global_properties().configuration.min_transaction_cpu_usage, true );
push_transaction( onbtrx, fc::time_point::maximum(), self.get_global_properties().configuration.min_transaction_cpu_usage, true, 0 );
} catch( const std::bad_alloc& e ) {
elog( "on block transaction failed due to a std::bad_alloc" );
throw;
Expand Down Expand Up @@ -1903,7 +1906,7 @@ struct controller_impl {
: ( !!std::get<0>( trx_metas.at( packed_idx ) ) ?
std::get<0>( trx_metas.at( packed_idx ) )
: std::get<1>( trx_metas.at( packed_idx ) ).get() ) );
trace = push_transaction( trx_meta, fc::time_point::maximum(), receipt.cpu_usage_us, true );
trace = push_transaction( trx_meta, fc::time_point::maximum(), receipt.cpu_usage_us, true, 0 );
++packed_idx;
} else if( receipt.trx.contains<transaction_id_type>() ) {
trace = push_scheduled_transaction( receipt.trx.get<transaction_id_type>(), fc::time_point::maximum(), receipt.cpu_usage_us, true );
Expand Down Expand Up @@ -2688,11 +2691,12 @@ void controller::push_block( std::future<block_state_ptr>& block_state_future,
}

transaction_trace_ptr controller::push_transaction( const transaction_metadata_ptr& trx, fc::time_point deadline,
uint32_t billed_cpu_time_us, bool explicit_billed_cpu_time ) {
uint32_t billed_cpu_time_us, bool explicit_billed_cpu_time,
uint32_t subjective_cpu_bill_us ) {
validate_db_available_size();
EOS_ASSERT( get_read_mode() != db_read_mode::IRREVERSIBLE, transaction_type_exception, "push transaction not allowed in irreversible mode" );
EOS_ASSERT( trx && !trx->implicit && !trx->scheduled, transaction_type_exception, "Implicit/Scheduled transaction not allowed" );
return my->push_transaction(trx, deadline, billed_cpu_time_us, explicit_billed_cpu_time );
return my->push_transaction(trx, deadline, billed_cpu_time_us, explicit_billed_cpu_time, subjective_cpu_bill_us );
}

transaction_trace_ptr controller::push_scheduled_transaction( const transaction_id_type& trxid, fc::time_point deadline,
Expand Down
3 changes: 2 additions & 1 deletion libraries/chain/include/eosio/chain/controller.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ namespace eosio { namespace chain {
*
*/
transaction_trace_ptr push_transaction( const transaction_metadata_ptr& trx, fc::time_point deadline,
uint32_t billed_cpu_time_us, bool explicit_billed_cpu_time );
uint32_t billed_cpu_time_us, bool explicit_billed_cpu_time,
uint32_t subjective_cpu_bill_us );

/**
* Attempt to execute a specific transaction in our deferred trx database
Expand Down
2 changes: 2 additions & 0 deletions libraries/chain/include/eosio/chain/resource_limits.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ namespace eosio { namespace chain { namespace resource_limits {
bool set_account_limits( const account_name& account, int64_t ram_bytes, int64_t net_weight, int64_t cpu_weight);
void get_account_limits( const account_name& account, int64_t& ram_bytes, int64_t& net_weight, int64_t& cpu_weight) const;

bool is_unlimited_cpu( const account_name& account ) const;

void process_account_limit_updates();
void process_block_usage( uint32_t block_num );

Expand Down
65 changes: 65 additions & 0 deletions libraries/chain/include/eosio/chain/resource_limits_private.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,71 @@ namespace eosio { namespace chain { namespace resource_limits {
}
};

/**
* This class accumulates a value that decays over quantums based on inputs
* The decay is linear between updates and exponential if the set of inputs has no gaps
*
* The value stored is Precision times the sum of the inputs.
*/
template<uint64_t Precision = config::rate_limiting_precision>
struct exponential_decay_accumulator
{
static_assert( Precision > 0, "Precision must be positive" );
static constexpr uint64_t max_raw_value = std::numeric_limits<uint64_t>::max() / Precision;

exponential_decay_accumulator()
: last_ordinal(0)
, value_ex(0)
{
}

uint32_t last_ordinal; ///< The ordinal of the last period which has contributed to the accumulator
uint64_t value_ex; ///< The current accumulated value pre-multiplied by Precision

/**
* return the extended value at a current or future ordinal
*/
uint64_t value_ex_at( uint32_t ordinal, uint32_t window_size ) const {
if( last_ordinal < ordinal ) {
if( (uint64_t)last_ordinal + window_size > (uint64_t)ordinal ) {
const auto delta = ordinal - last_ordinal; // clearly 0 < delta < window_size
const auto decay = make_ratio(
(uint128_t)window_size - delta,
(uint128_t)window_size
);

return downgrade_cast<uint64_t>((uint128_t)value_ex * decay);
} else {
return 0;
}
} else {
return value_ex;
}
}

/**
* return the value at a current or future ordinal
*/
uint64_t value_at( uint32_t ordinal, uint32_t window_size ) const {
return integer_divide_ceil(value_ex_at(ordinal, window_size), Precision);
}

void add( uint64_t units, uint32_t ordinal, uint32_t window_size /* must be positive */ )
{
// check for some numerical limits before doing any state mutations
EOS_ASSERT(units <= max_raw_value, rate_limiting_state_inconsistent, "Usage exceeds maximum value representable after extending for precision");

uint128_t units_ex = (uint128_t)units * Precision;
if (last_ordinal < ordinal) {
value_ex = value_ex_at(ordinal, window_size);
last_ordinal = ordinal;
}

// saturate the value
uint128_t new_value_ex = std::min<uint128_t>(units_ex + (uint128_t)value_ex, std::numeric_limits<uint64_t>::max());
value_ex = downgrade_cast<uint64_t>(new_value_ex);
}
};
}

using usage_accumulator = impl::exponential_moving_average_accumulator<>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ namespace eosio { namespace chain {
fc::time_point deadline = fc::time_point::maximum();
fc::microseconds leeway = fc::microseconds( config::default_subjective_cpu_leeway_us );
int64_t billed_cpu_time_us = 0;
uint32_t subjective_cpu_bill_us = 0;
bool explicit_billed_cpu_time = false;

transaction_checktime_timer transaction_timer;
Expand Down
7 changes: 7 additions & 0 deletions libraries/chain/resource_limits.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,13 @@ void resource_limits_manager::get_account_limits( const account_name& account, i
}
}

bool resource_limits_manager::is_unlimited_cpu( const account_name& account ) const {
const auto* buo = _db.find<resource_limits_object,by_owner>( boost::make_tuple(false, account) );
if (buo) {
return buo->cpu_weight == -1;
}
return false;
}

void resource_limits_manager::process_account_limit_updates() {
auto& multi_index = _db.get_mutable_index<resource_limits_index>();
Expand Down
4 changes: 3 additions & 1 deletion libraries/chain/transaction_context.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,9 @@ namespace eosio { namespace chain {

if( !explicit_billed_cpu_time ) {
// Fail early if amount of the previous speculative execution is within 10% of remaining account cpu available
int64_t validate_account_cpu_limit = account_cpu_limit - EOS_PERCENT( account_cpu_limit, 10 * config::percent_1 );
int64_t validate_account_cpu_limit = account_cpu_limit - subjective_cpu_bill_us;
if( validate_account_cpu_limit > 0 )
validate_account_cpu_limit -= EOS_PERCENT( validate_account_cpu_limit, 10 * config::percent_1 );
if( validate_account_cpu_limit < 0 ) validate_account_cpu_limit = 0;
validate_account_cpu_usage( billed_cpu_time_us, validate_account_cpu_limit, true );
}
Expand Down
6 changes: 3 additions & 3 deletions libraries/testing/tester.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ namespace eosio { namespace testing {

if( !skip_pending_trxs ) {
for( auto itr = unapplied_transactions.begin(); itr != unapplied_transactions.end(); ) {
auto trace = control->push_transaction( itr->trx_meta, fc::time_point::maximum(), DEFAULT_BILLED_CPU_TIME_US, true );
auto trace = control->push_transaction( itr->trx_meta, fc::time_point::maximum(), DEFAULT_BILLED_CPU_TIME_US, true, 0 );
traces.emplace_back( trace );
if(!no_throw && trace->except) {
// this always throws an fc::exception, since the original exception is copied into an fc::exception
Expand Down Expand Up @@ -549,7 +549,7 @@ namespace eosio { namespace testing {
fc::microseconds::maximum() :
fc::microseconds( deadline - fc::time_point::now() );
auto fut = transaction_metadata::start_recover_keys( ptrx, control->get_thread_pool(), control->get_chain_id(), time_limit );
auto r = control->push_transaction( fut.get(), deadline, billed_cpu_time_us, billed_cpu_time_us > 0 );
auto r = control->push_transaction( fut.get(), deadline, billed_cpu_time_us, billed_cpu_time_us > 0, 0 );
if( r->except_ptr ) std::rethrow_exception( r->except_ptr );
if( r->except ) throw *r->except;
return r;
Expand All @@ -574,7 +574,7 @@ namespace eosio { namespace testing {
fc::microseconds( deadline - fc::time_point::now() );
auto ptrx = std::make_shared<packed_transaction>( trx, c );
auto fut = transaction_metadata::start_recover_keys( ptrx, control->get_thread_pool(), control->get_chain_id(), time_limit );
auto r = control->push_transaction( fut.get(), deadline, billed_cpu_time_us, billed_cpu_time_us > 0 );
auto r = control->push_transaction( fut.get(), deadline, billed_cpu_time_us, billed_cpu_time_us > 0, 0 );
if (no_throw) return r;
if( r->except_ptr ) std::rethrow_exception( r->except_ptr );
if( r->except) throw *r->except;
Expand Down
81 changes: 62 additions & 19 deletions plugins/chain_plugin/account_query_db.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ using namespace boost::bimaps;
namespace {
/**
* Structure to hold indirect reference to a `property_object` via {owner,name} as well as a non-standard
* index over `last_updated` for roll-back support
* index over `last_updated_height` (which is truncated at the LIB during initialization) for roll-back support
*/
struct permission_info {
// indexed data
chain::name owner;
chain::name name;
fc::time_point last_updated;
uint32_t last_updated_height;

// un-indexed data
uint32_t threshold;
Expand All @@ -37,10 +37,10 @@ namespace {
};

struct by_owner_name;
struct by_last_updated;
struct by_last_updated_height;

/**
* Multi-index providing fast lookup for {owner,name} as well as {last_updated}
* Multi-index providing fast lookup for {owner,name} as well as {last_updated_height}
*/
using permission_info_index_t = multi_index_container<
permission_info,
Expand All @@ -53,8 +53,8 @@ namespace {
>
>,
ordered_non_unique<
tag<by_last_updated>,
member<permission_info, fc::time_point, &permission_info::last_updated>
tag<by_last_updated_height>,
member<permission_info, uint32_t, &permission_info::last_updated_height>
>
>
>;
Expand Down Expand Up @@ -143,8 +143,19 @@ namespace eosio::chain_apis {
auto start = fc::time_point::now();
const auto& index = controller.db().get_index<chain::permission_index>().indices().get<by_id>();

// build a initial time to block number map
const auto lib_num = controller.last_irreversible_block_num();
const auto head_num = controller.head_block_num();

for (uint32_t block_num = lib_num + 1; block_num <= head_num; block_num++) {
const auto block_p = controller.fetch_block_by_number(block_num);
EOS_ASSERT(block_p, chain::plugin_exception, "cannot fetch reversible block ${block_num}, required for account_db initialization", ("block_num", block_num));
time_to_block_num.emplace(block_p->timestamp.to_time_point(), block_num);
}

for (const auto& po : index ) {
const auto& pi = permission_info_index.emplace( permission_info{ po.owner, po.name, po.last_updated, po.auth.threshold } ).first;
uint32_t last_updated_height = last_updated_time_to_height(po.last_updated);
const auto& pi = permission_info_index.emplace( permission_info{ po.owner, po.name, last_updated_height, po.auth.threshold } ).first;
add_to_bimaps(*pi, po);
}
auto duration = fc::time_point::now() - start;
Expand Down Expand Up @@ -185,37 +196,57 @@ namespace eosio::chain_apis {

bool is_rollback_required( const chain::block_state_ptr& bsp ) const {
std::shared_lock read_lock(rw_mutex);
const auto t = bsp->block->timestamp.to_time_point();
const auto& index = permission_info_index.get<by_last_updated>();
const auto bnum = bsp->block->block_num();
const auto& index = permission_info_index.get<by_last_updated_height>();

if (index.empty()) {
return false;
} else {
const auto& pi = (*index.rbegin());
if (pi.last_updated < t) {
if (pi.last_updated_height < bnum) {
return false;
}
}

return true;
}

uint32_t last_updated_time_to_height( const fc::time_point& last_updated) {
const auto lib_num = controller.last_irreversible_block_num();
const auto lib_time = controller.last_irreversible_block_time();

uint32_t last_updated_height = lib_num;
if (last_updated > lib_time) {
const auto iter = time_to_block_num.find(last_updated);
EOS_ASSERT(iter != time_to_block_num.end(), chain::plugin_exception, "invalid block time encountered in on-chain accounts ${time}", ("time", last_updated));
last_updated_height = iter->second;
}

return last_updated_height;
}

/**
* Given a time_point, remove all permissions that were last updated at or after that time_point
* this will effectively remove any updates that happened at or after that time point
* Given a block number, remove all permissions that were last updated at or after that block number
* this will effectively roll back the database to just before the incoming block
*
* For each removed entry, this will create a new entry if there exists an equivalent {owner, name} permission
* at the HEAD state of the chain.
* @param bsp - the block to rollback before
*/
void rollback_to_before( const chain::block_state_ptr& bsp ) {
const auto t = bsp->block->timestamp.to_time_point();
auto& index = permission_info_index.get<by_last_updated>();
const auto bnum = bsp->block->block_num();
auto& index = permission_info_index.get<by_last_updated_height>();
const auto& permission_by_owner = controller.db().get_index<chain::permission_index>().indices().get<chain::by_owner>();

// roll back time-map
auto time_iter = time_to_block_num.rbegin();
while (time_iter != time_to_block_num.rend() && time_iter->second >= bnum) {
time_iter = decltype(time_iter){time_to_block_num.erase( std::next(time_iter).base() )};
}

while (!index.empty()) {
const auto& pi = (*index.rbegin());
if (pi.last_updated < t) {
if (pi.last_updated_height < bnum) {
break;
}

Expand All @@ -228,8 +259,11 @@ namespace eosio::chain_apis {
index.erase(index.iterator_to(pi));
} else {
const auto& po = *itr;
index.modify(index.iterator_to(pi), [&po](auto& mutable_pi) {
mutable_pi.last_updated = po.last_updated;

uint32_t last_updated_height = po.last_updated == bsp->header.timestamp ? bsp->block_num : last_updated_time_to_height(po.last_updated);

index.modify(index.iterator_to(pi), [&po, last_updated_height](auto& mutable_pi) {
mutable_pi.last_updated_height = last_updated_height;
mutable_pi.threshold = po.auth.threshold;
});
add_to_bimaps(pi, po);
Expand Down Expand Up @@ -331,6 +365,11 @@ namespace eosio::chain_apis {
std::unique_lock write_lock(rw_mutex);

rollback_to_before(bsp);

// insert this blocks time into the time map
time_to_block_num.emplace(bsp->header.timestamp, bsp->block_num);

const auto bnum = bsp->block_num;
auto& index = permission_info_index.get<by_owner_name>();
const auto& permission_by_owner = controller.db().get_index<chain::permission_index>().indices().get<chain::by_owner>();

Expand All @@ -342,11 +381,11 @@ namespace eosio::chain_apis {
auto itr = index.find(key);
if (itr == index.end()) {
const auto& po = *source_itr;
itr = index.emplace(permission_info{ po.owner, po.name, po.last_updated, po.auth.threshold }).first;
itr = index.emplace(permission_info{ po.owner, po.name, bnum, po.auth.threshold }).first;
} else {
remove_from_bimaps(*itr);
index.modify(itr, [&](auto& mutable_pi){
mutable_pi.last_updated = source_itr->last_updated;
mutable_pi.last_updated_height = bnum;
mutable_pi.threshold = source_itr->auth.threshold;
});
}
Expand Down Expand Up @@ -440,6 +479,10 @@ namespace eosio::chain_apis {
cached_trace_map_t cached_trace_map; ///< temporary cache of uncommitted traces
onblock_trace_t onblock_trace; ///< temporary cache of on_block trace

using time_map_t = std::map<fc::time_point, uint32_t>;
time_map_t time_to_block_num;



using name_bimap_t = bimap<multiset_of<weighted<chain::permission_level>>, multiset_of<permission_info::cref>>;
using key_bimap_t = bimap<multiset_of<weighted<chain::public_key_type>>, multiset_of<permission_info::cref>>;
Expand Down
Loading

0 comments on commit 9c254a9

Please sign in to comment.